From 26dd78415366676246f144bbe10040e0550bbef4 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 3 Dec 2024 13:19:17 +0100 Subject: [PATCH 001/291] gobj: Make BufferResidencyTracker::_residency a const field --- panda/src/gobj/bufferContext.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/gobj/bufferContext.h b/panda/src/gobj/bufferContext.h index 60c9ab3333..0d6d49b5e4 100644 --- a/panda/src/gobj/bufferContext.h +++ b/panda/src/gobj/bufferContext.h @@ -73,7 +73,7 @@ protected: TypedWritableReferenceCount *_object; private: - BufferResidencyTracker *_residency; + BufferResidencyTracker *const _residency; int _residency_state; size_t _data_size_bytes; From 6f33bbecea7b1a3dd286c8131af36cace70a6229 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 3 Dec 2024 13:19:48 +0100 Subject: [PATCH 002/291] glgsg: Fix use after free with gl-force-fbo-color and layered FBO --- panda/src/glstuff/glGraphicsBuffer_src.cxx | 3 +++ panda/src/glstuff/glGraphicsBuffer_src.h | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/panda/src/glstuff/glGraphicsBuffer_src.cxx b/panda/src/glstuff/glGraphicsBuffer_src.cxx index cd87938288..cfe143214d 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.cxx +++ b/panda/src/glstuff/glGraphicsBuffer_src.cxx @@ -419,6 +419,7 @@ rebuild_bitplanes() { Texture *attach[RTP_COUNT]; memset(attach, 0, sizeof(Texture *) * RTP_COUNT); _texture_contexts.clear(); + _textures.clear(); // Sort the textures list into appropriate slots. { @@ -793,6 +794,8 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot, _fb_properties.setup_color_texture(tex); } + _textures.push_back(tex); + TextureContext *tc = tex->prepare_now(glgsg->get_prepared_objects(), glgsg); nassertv(tc != nullptr); CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc); diff --git a/panda/src/glstuff/glGraphicsBuffer_src.h b/panda/src/glstuff/glGraphicsBuffer_src.h index ab07322293..f718e134f8 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.h +++ b/panda/src/glstuff/glGraphicsBuffer_src.h @@ -128,6 +128,10 @@ protected: typedef pvector TextureContexts; TextureContexts _texture_contexts; + // List of textures we need to keep a reference to. + typedef pvector Textures; + Textures _textures; + // The cube map face we are currently drawing to or have just finished // drawing to, or -1 if we are not drawing to a cube map. int _bound_tex_page; From 5e0504972568eda733bacbab7e6ff0c4e5d2585f Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 14 Dec 2024 12:48:24 +0100 Subject: [PATCH 003/291] collide: Add custom owner field to CollisionNode This stores a weakref to a custom Python object, useful for being able to look up the game object corresponding to a particular collision node in an event without the downsides of Python tags Fixes #1703 --- panda/src/collide/CMakeLists.txt | 2 + panda/src/collide/collisionNode.I | 21 +++++++ panda/src/collide/collisionNode.cxx | 24 ++++++- panda/src/collide/collisionNode.h | 19 ++++++ panda/src/collide/collisionNode_ext.cxx | 62 +++++++++++++++++++ panda/src/collide/collisionNode_ext.h | 40 ++++++++++++ panda/src/collide/p3collide_ext_composite.cxx | 1 + panda/src/pgraph/nodePath.h | 2 + panda/src/pgraph/nodePath_ext.cxx | 59 ++++++++++++++++++ panda/src/pgraph/nodePath_ext.h | 6 ++ tests/collide/test_collision_node.py | 46 ++++++++++++++ tests/pgraph/test_nodepath.py | 44 +++++++++++++ 12 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 panda/src/collide/collisionNode_ext.cxx create mode 100644 panda/src/collide/collisionNode_ext.h create mode 100644 tests/collide/test_collision_node.py diff --git a/panda/src/collide/CMakeLists.txt b/panda/src/collide/CMakeLists.txt index 296a40eeed..8dad7fe29a 100644 --- a/panda/src/collide/CMakeLists.txt +++ b/panda/src/collide/CMakeLists.txt @@ -74,6 +74,8 @@ set(P3COLLIDE_IGATEEXT collisionHandlerPhysical_ext.h collisionHandlerQueue_ext.cxx collisionHandlerQueue_ext.h + collisionNode_ext.cxx + collisionNode_ext.h collisionPolygon_ext.cxx collisionPolygon_ext.h collisionTraverser_ext.cxx diff --git a/panda/src/collide/collisionNode.I b/panda/src/collide/collisionNode.I index 51fe8002fb..f429fccfb1 100644 --- a/panda/src/collide/collisionNode.I +++ b/panda/src/collide/collisionNode.I @@ -166,3 +166,24 @@ INLINE CollideMask CollisionNode:: get_default_collide_mask() { return default_collision_node_collide_mask; } + +/** + * Returns the custom pointer set via set_owner(). + */ +INLINE void *CollisionNode:: +get_owner() const { + return _owner; +} + +/** + * Sets a custom pointer, together with an optional callback that will be + * called when the node is deleted. + * + * The owner or the callback will not be copied along with the CollisionNode. + */ +INLINE void CollisionNode:: +set_owner(void *owner, OwnerCallback *callback) { + clear_owner(); + _owner = owner; + _owner_callback = callback; +} diff --git a/panda/src/collide/collisionNode.cxx b/panda/src/collide/collisionNode.cxx index 2df893ef18..60d28960dd 100644 --- a/panda/src/collide/collisionNode.cxx +++ b/panda/src/collide/collisionNode.cxx @@ -40,7 +40,9 @@ CollisionNode:: CollisionNode(const std::string &name) : PandaNode(name), _from_collide_mask(get_default_collide_mask()), - _collider_sort(0) + _collider_sort(0), + _owner(nullptr), + _owner_callback(nullptr) { set_cull_callback(); set_renderable(); @@ -60,7 +62,9 @@ CollisionNode(const CollisionNode ©) : PandaNode(copy), _from_collide_mask(copy._from_collide_mask), _collider_sort(copy._collider_sort), - _solids(copy._solids) + _solids(copy._solids), + _owner(nullptr), + _owner_callback(nullptr) { } @@ -69,6 +73,10 @@ CollisionNode(const CollisionNode ©) : */ CollisionNode:: ~CollisionNode() { + if (_owner_callback != nullptr) { + _owner_callback(_owner); + _owner_callback = nullptr; + } } /** @@ -251,6 +259,18 @@ set_from_collide_mask(CollideMask mask) { _from_collide_mask = mask; } +/** + * Removes the owner that was previously set using set_owner(). + */ +void CollisionNode:: +clear_owner() { + if (_owner_callback != nullptr) { + _owner_callback(_owner); + } + _owner = nullptr; + _owner_callback = nullptr; +} + /** * Called when needed to recompute the node's _internal_bound object. Nodes * that contain anything of substance should redefine this to do the right diff --git a/panda/src/collide/collisionNode.h b/panda/src/collide/collisionNode.h index fee71fce78..14941a5336 100644 --- a/panda/src/collide/collisionNode.h +++ b/panda/src/collide/collisionNode.h @@ -77,6 +77,22 @@ PUBLISHED: INLINE static CollideMask get_default_collide_mask(); MAKE_PROPERTY(default_collide_mask, get_default_collide_mask); +public: + typedef void (OwnerCallback)(void *); + + INLINE void *get_owner() const; + +#ifndef CPPPARSER + INLINE void set_owner(void *owner, OwnerCallback *callback = nullptr); + void clear_owner(); +#endif + + EXTENSION(PyObject *get_owner() const); + EXTENSION(void set_owner(PyObject *owner)); + +PUBLISHED: + MAKE_PROPERTY(owner, get_owner, set_owner); + protected: virtual void compute_internal_bounds(CPT(BoundingVolume) &internal_bounds, int &internal_vertices, @@ -94,6 +110,9 @@ private: typedef pvector< COWPT(CollisionSolid) > Solids; Solids _solids; + void *_owner = nullptr; + OwnerCallback *_owner_callback = nullptr; + friend class CollisionTraverser; public: diff --git a/panda/src/collide/collisionNode_ext.cxx b/panda/src/collide/collisionNode_ext.cxx new file mode 100644 index 0000000000..ae42a0d7ef --- /dev/null +++ b/panda/src/collide/collisionNode_ext.cxx @@ -0,0 +1,62 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file collisionNode_ext.cxx + * @author rdb + * @date 2024-12-12 + */ + +#include "collisionNode_ext.h" + +#ifdef HAVE_PYTHON + +#include "collisionNode.h" + +/** + * Returns the object previously set via set_owner(). If the object has been + * destroyed, returns None. + */ +PyObject *Extension:: +get_owner() const { + PyObject *owner = (PyObject *)_this->get_owner(); + +#if PY_VERSION_HEX >= 0x030D0000 // 3.13 + PyObject *strong_ref; + int result = 0; + if (owner != nullptr) { + PyWeakref_GetRef(owner, &strong_ref); + } + if (result > 0) { + return strong_ref; + } + else if (result == 0) { + return Py_NewRef(Py_None); + } + else { + return nullptr; + } +#else + return Py_NewRef(owner != nullptr ? PyWeakref_GetObject(owner) : Py_None); +#endif +} + +/** + * Stores a weak reference to the given object on the CollisionNode, for later + * use in collision events and handlers. + */ +void Extension:: +set_owner(PyObject *owner) { + if (owner != Py_None) { + PyObject *ref = PyWeakref_NewRef(owner, nullptr); + _this->set_owner(ref, [](void *obj) { Py_DECREF((PyObject *)obj); }); + } else { + _this->clear_owner(); + } +} + +#endif diff --git a/panda/src/collide/collisionNode_ext.h b/panda/src/collide/collisionNode_ext.h new file mode 100644 index 0000000000..ea27e1bb7d --- /dev/null +++ b/panda/src/collide/collisionNode_ext.h @@ -0,0 +1,40 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file collisionNode_ext.h + * @author rdb + * @date 2024-12-12 + */ + +#ifndef COLLISIONNODE_EXT_H +#define COLLISIONNODE_EXT_H + +#include "pandabase.h" + +#ifdef HAVE_PYTHON + +#include "extension.h" +#include "collisionNode.h" +#include "py_panda.h" + +/** + * This class defines the extension methods for CollisionNode, which are called + * instead of any C++ methods with the same prototype. + * + * @since 1.11.0 + */ +template<> +class Extension : public ExtensionBase { +public: + PyObject *get_owner() const; + void set_owner(PyObject *owner); +}; + +#endif // HAVE_PYTHON + +#endif diff --git a/panda/src/collide/p3collide_ext_composite.cxx b/panda/src/collide/p3collide_ext_composite.cxx index 76e458c0f3..8e9f584c0c 100644 --- a/panda/src/collide/p3collide_ext_composite.cxx +++ b/panda/src/collide/p3collide_ext_composite.cxx @@ -1,5 +1,6 @@ #include "collisionHandlerEvent_ext.cxx" #include "collisionHandlerPhysical_ext.cxx" #include "collisionHandlerQueue_ext.cxx" +#include "collisionNode_ext.cxx" #include "collisionPolygon_ext.cxx" #include "collisionTraverser_ext.cxx" diff --git a/panda/src/pgraph/nodePath.h b/panda/src/pgraph/nodePath.h index b8f82f779f..21328a5888 100644 --- a/panda/src/pgraph/nodePath.h +++ b/panda/src/pgraph/nodePath.h @@ -884,6 +884,8 @@ PUBLISHED: INLINE void set_collide_mask(CollideMask new_mask, CollideMask bits_to_change = CollideMask::all_on(), TypeHandle node_type = TypeHandle::none()); + EXTENSION(void set_collide_owner(PyObject *owner)); + // Comparison methods INLINE bool operator == (const NodePath &other) const; INLINE bool operator != (const NodePath &other) const; diff --git a/panda/src/pgraph/nodePath_ext.cxx b/panda/src/pgraph/nodePath_ext.cxx index e05877be53..060a96e44d 100644 --- a/panda/src/pgraph/nodePath_ext.cxx +++ b/panda/src/pgraph/nodePath_ext.cxx @@ -15,6 +15,7 @@ #include "typedWritable_ext.h" #include "shaderInput_ext.h" #include "shaderAttrib.h" +#include "collisionNode.h" #ifdef HAVE_PYTHON @@ -327,4 +328,62 @@ get_tight_bounds(const NodePath &other) const { } } +/** + * Recursively assigns a weak reference to the given owner object to all + * collision nodes at this level and below. + * + * You may pass in None to clear all owners below this level. + * + * Note that there is no corresponding get_collide_owner(), since there may be + * multiple nodes below this level with different owners. + */ +void Extension:: +set_collide_owner(PyObject *owner) { + if (owner != Py_None) { + PyObject *ref = PyWeakref_NewRef(owner, nullptr); + if (ref != nullptr) { + r_set_collide_owner(_this->node(), ref); + Py_DECREF(ref); + } + } else { + r_clear_collide_owner(_this->node()); + } +} + +/** + * Recursive implementation of set_collide_owner. weakref must be a weak ref + * object. + */ +void Extension:: +r_set_collide_owner(PandaNode *node, PyObject *weakref) { + if (node->is_collision_node()) { + CollisionNode *cnode = (CollisionNode *)node; + cnode->set_owner(Py_NewRef(weakref), + [](void *obj) { Py_DECREF((PyObject *)obj); }); + } + + PandaNode::Children cr = node->get_children(); + int num_children = cr.get_num_children(); + for (int i = 0; i < num_children; i++) { + r_set_collide_owner(cr.get_child(i), weakref); + } +} + +/** + * Recursive implementation of set_collide_owner(None). + */ +void Extension:: +r_clear_collide_owner(PandaNode *node) { + if (node->is_collision_node()) { + CollisionNode *cnode = (CollisionNode *)node; + cnode->clear_owner(); + } + + PandaNode::Children cr = node->get_children(); + int num_children = cr.get_num_children(); + for (int i = 0; i < num_children; i++) { + r_clear_collide_owner(cr.get_child(i)); + } +} + #endif // HAVE_PYTHON diff --git a/panda/src/pgraph/nodePath_ext.h b/panda/src/pgraph/nodePath_ext.h index b1127f930d..b6f03b792c 100644 --- a/panda/src/pgraph/nodePath_ext.h +++ b/panda/src/pgraph/nodePath_ext.h @@ -54,6 +54,12 @@ public: void set_shader_inputs(PyObject *args, PyObject *kwargs); PyObject *get_tight_bounds(const NodePath &other = NodePath()) const; + + void set_collide_owner(PyObject *owner); + +private: + static void r_set_collide_owner(PandaNode *node, PyObject *weakref); + static void r_clear_collide_owner(PandaNode *node); }; BEGIN_PUBLISH diff --git a/tests/collide/test_collision_node.py b/tests/collide/test_collision_node.py new file mode 100644 index 0000000000..20197e956f --- /dev/null +++ b/tests/collide/test_collision_node.py @@ -0,0 +1,46 @@ +from panda3d.core import * +import sys + + +class CustomObject: + pass + + +def test_collision_node_owner(): + owner = CustomObject() + initial_rc = sys.getrefcount(owner) + + node = CollisionNode("node") + assert node.owner is None + + node.owner = owner + assert sys.getrefcount(owner) == initial_rc + assert node.owner is owner + + node.owner = owner + assert sys.getrefcount(owner) == initial_rc + assert node.owner is owner + + node.owner = None + assert sys.getrefcount(owner) == initial_rc + assert node.owner is None + + del node + assert sys.getrefcount(owner) == initial_rc + + # Assign owner and then delete node + node = CollisionNode("node") + assert sys.getrefcount(owner) == initial_rc + node.owner = owner + assert sys.getrefcount(owner) == initial_rc + del node + assert sys.getrefcount(owner) == initial_rc + + # Delete owner and see what happens to the node + node = CollisionNode("node") + assert sys.getrefcount(owner) == initial_rc + node.owner = owner + assert sys.getrefcount(owner) == initial_rc + del owner + assert node.owner is None + diff --git a/tests/pgraph/test_nodepath.py b/tests/pgraph/test_nodepath.py index ef9d44ab33..f426530429 100644 --- a/tests/pgraph/test_nodepath.py +++ b/tests/pgraph/test_nodepath.py @@ -312,3 +312,47 @@ def test_nodepath_replace_texture_none(): assert path2.get_texture() == tex1 path1.replace_texture(tex1, None) assert path2.get_texture() is None + + +def test_nodepath_set_collide_owner(): + from panda3d.core import NodePath, CollisionNode + + class CustomOwner: + pass + + owner1 = CustomOwner() + owner2 = CustomOwner() + owner3 = CustomOwner() + + root = NodePath("root") + model1 = root.attach_new_node("model1") + collider1 = model1.attach_new_node(CollisionNode("collider1")) + collider2 = model1.attach_new_node(CollisionNode("collider2")) + model2 = root.attach_new_node("model2") + collider3 = model2.attach_new_node(CollisionNode("collider3")) + + root.set_collide_owner(owner1) + assert collider1.node().owner is owner1 + assert collider2.node().owner is owner1 + assert collider3.node().owner is owner1 + + model1.set_collide_owner(None) + assert collider1.node().owner is None + assert collider2.node().owner is None + assert collider3.node().owner is owner1 + + collider2.set_collide_owner(owner2) + assert collider1.node().owner is None + assert collider2.node().owner is owner2 + assert collider3.node().owner is owner1 + + del owner1 + assert collider1.node().owner is None + assert collider2.node().owner is owner2 + assert collider3.node().owner is None + + root.set_collide_owner(owner3) + model2.set_collide_owner(owner2) + assert collider1.node().owner is owner3 + assert collider2.node().owner is owner3 + assert collider3.node().owner is owner2 From 5d465be01e9ac5a8aad9cb689af822f60682b7e9 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 14 Dec 2024 12:50:02 +0100 Subject: [PATCH 004/291] samples: Remove unused CardMaker import from procedural-cube sample --- samples/procedural-cube/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/procedural-cube/main.py b/samples/procedural-cube/main.py index 2f21cd9a0f..266afc4205 100755 --- a/samples/procedural-cube/main.py +++ b/samples/procedural-cube/main.py @@ -17,7 +17,6 @@ from panda3d.core import GeomVertexFormat, GeomVertexData from panda3d.core import Geom, GeomTriangles, GeomVertexWriter from panda3d.core import Texture, GeomNode from panda3d.core import PerspectiveLens -from panda3d.core import CardMaker from panda3d.core import Light, Spotlight from panda3d.core import TextNode from panda3d.core import LVector3 From 343ec6b3673f1520882da3b79cb9e53c6516df14 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 14 Dec 2024 12:57:27 +0100 Subject: [PATCH 005/291] showbase: Create Loader before ShowBase, importable from ShowBaseGlobal This makes it possible to load models before opening ShowBase --- direct/src/showbase/Loader.py | 11 ++++++++--- direct/src/showbase/ShowBase.py | 4 ++-- direct/src/showbase/ShowBaseGlobal.py | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index 141c71a368..0a1e60f01e 100644 --- a/direct/src/showbase/Loader.py +++ b/direct/src/showbase/Loader.py @@ -126,7 +126,7 @@ class Loader(DirectObject): yield await req # special methods - def __init__(self, base): + def __init__(self, base=None): self.base = base self.loader = PandaLoader.getGlobalPtr() @@ -136,8 +136,6 @@ class Loader(DirectObject): Loader.loaderIndex += 1 self.accept(self.hook, self.__gotAsyncObject) - self._loadPythonFileTypes() - def destroy(self): self.ignore(self.hook) self.loader.stopThreads() @@ -229,6 +227,10 @@ class Loader(DirectObject): """ assert Loader.notify.debug("Loading model: %s" % (modelPath,)) + + if not self._loadedPythonFileTypes: + self._loadPythonFileTypes() + if loaderOptions is None: loaderOptions = LoaderOptions() else: @@ -416,6 +418,9 @@ class Loader(DirectObject): a callback is used, the model is saved asynchronously, and the true/false status is passed to the callback function. """ + if not self._loadedPythonFileTypes: + self._loadPythonFileTypes() + if loaderOptions is None: loaderOptions = LoaderOptions() else: diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 0ffbc6fba8..f35d7dea2a 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -436,9 +436,9 @@ class ShowBase(DirectObject.DirectObject): self.useTrackball() #: `.Loader.Loader` object. - self.loader = Loader.Loader(self) + self.loader = ShowBaseGlobal.loader + self.loader.base = self self.graphicsEngine.setDefaultLoader(self.loader.loader) - ShowBaseGlobal.loader = self.loader #: The global event manager, as imported from `.EventManagerGlobal`. self.eventMgr = eventMgr diff --git a/direct/src/showbase/ShowBaseGlobal.py b/direct/src/showbase/ShowBaseGlobal.py index 4cd4e8623b..fe891e9c1b 100644 --- a/direct/src/showbase/ShowBaseGlobal.py +++ b/direct/src/showbase/ShowBaseGlobal.py @@ -62,7 +62,8 @@ aspect2d = render2d.attachNewNode(PGTop("aspect2d")) #: A dummy scene graph that is not being rendered by anything. hidden = NodePath("hidden") -loader: Loader +#: The global Loader instance for models, textures, etc. +loader = Loader() # Set direct notify categories now that we have config directNotify.setDconfigLevels() From 3f2b9cd351ffe9ceaa849d556fd23340195e08f1 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 14 Dec 2024 13:25:12 +0100 Subject: [PATCH 006/291] pgraph: Make ModelPool.load_model() work same as Loader.load_sync() ModelPool.load_model() did not work like the other pools, didn't resolve relative to the VFS, didn't respect loader options... let's just make these work the same. We should deprecate one in favor of the other eventually --- panda/src/pgraph/loader.cxx | 18 +++++++++++------- panda/src/pgraph/modelPool.cxx | 31 ++++--------------------------- 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/panda/src/pgraph/loader.cxx b/panda/src/pgraph/loader.cxx index 15b130592d..a051aeaf35 100644 --- a/panda/src/pgraph/loader.cxx +++ b/panda/src/pgraph/loader.cxx @@ -392,13 +392,17 @@ try_load_file(const Filename &pathname, const LoaderOptions &options, sgr.premunge(result, RenderState::make_empty()); } - if (allow_ram_cache && result->is_of_type(ModelRoot::get_class_type())) { - // Store the loaded model in the RAM cache, and make sure we return a - // copy so that this node can be modified independently from the RAM - // cached version. - ModelPool::add_model(pathname, DCAST(ModelRoot, result.p())); - if ((options.get_flags() & LoaderOptions::LF_allow_instance) == 0) { - result = NodePath(result).copy_to(NodePath()).node(); + if (result->is_of_type(ModelRoot::get_class_type())) { + ((ModelRoot *)result.p())->set_fullpath(pathname); + + if (allow_ram_cache) { + // Store the loaded model in the RAM cache, and make sure we return a + // copy so that this node can be modified independently from the RAM + // cached version. + ModelPool::add_model(pathname, DCAST(ModelRoot, result.p())); + if ((options.get_flags() & LoaderOptions::LF_allow_instance) == 0) { + result = NodePath(result).copy_to(NodePath()).node(); + } } } diff --git a/panda/src/pgraph/modelPool.cxx b/panda/src/pgraph/modelPool.cxx index c53e2b867f..4985cd2b65 100644 --- a/panda/src/pgraph/modelPool.cxx +++ b/panda/src/pgraph/modelPool.cxx @@ -118,52 +118,29 @@ ns_get_model(const Filename &filename, bool verify) { */ ModelRoot *ModelPool:: ns_load_model(const Filename &filename, const LoaderOptions &options) { - - // First check if it has already been loaded and is still current. + // First check if it's been cached under the given filename (for backward + // compatibility reasons) PT(ModelRoot) cached_model = ns_get_model(filename, true); if (cached_model != nullptr) { return cached_model; } - // Look on disk for the current file. LoaderOptions new_options(options); - new_options.set_flags((new_options.get_flags() | LoaderOptions::LF_no_ram_cache) & - ~LoaderOptions::LF_search); + new_options.set_flags(new_options.get_flags() & ~LoaderOptions::LF_no_ram_cache); Loader *model_loader = Loader::get_global_ptr(); PT(PandaNode) panda_node = model_loader->load_sync(filename, new_options); PT(ModelRoot) node; - if (panda_node.is_null()) { - // This model was not found. - - } else { + if (!panda_node.is_null()) { if (panda_node->is_of_type(ModelRoot::get_class_type())) { node = DCAST(ModelRoot, panda_node); - } else { // We have to construct a ModelRoot node to put it under. node = new ModelRoot(filename); node->add_child(panda_node); } - node->set_fullpath(filename); } - - { - LightMutexHolder holder(_lock); - - // Look again, in case someone has just loaded the model in another - // thread. - Models::const_iterator ti; - ti = _models.find(filename); - if (ti != _models.end() && (*ti).second != cached_model) { - // This model was previously loaded. - return (*ti).second; - } - - _models[filename] = node; - } - return node; } From 46cf9c0507b3c93c4ea85ea0374ae81803051999 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 14 Dec 2024 14:47:28 +0100 Subject: [PATCH 007/291] showbase: Fix Loader when destroying and recreating ShowBase --- direct/src/showbase/Loader.py | 6 ++++-- direct/src/showbase/ShowBase.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index 0a1e60f01e..7febb9012d 100644 --- a/direct/src/showbase/Loader.py +++ b/direct/src/showbase/Loader.py @@ -134,13 +134,15 @@ class Loader(DirectObject): self.hook = "async_loader_%s" % (Loader.loaderIndex) Loader.loaderIndex += 1 - self.accept(self.hook, self.__gotAsyncObject) def destroy(self): self.ignore(self.hook) self.loader.stopThreads() del self.base - del self.loader + + def _init_base(self, base): + self.base = base + self.accept(self.hook, self.__gotAsyncObject) @classmethod def _loadPythonFileTypes(cls): diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index f35d7dea2a..40ebc8b8b9 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -437,7 +437,7 @@ class ShowBase(DirectObject.DirectObject): #: `.Loader.Loader` object. self.loader = ShowBaseGlobal.loader - self.loader.base = self + self.loader._init_base(self) self.graphicsEngine.setDefaultLoader(self.loader.loader) #: The global event manager, as imported from `.EventManagerGlobal`. From e43dc87df593d3673b2daebbda00683f830221af Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 14 Dec 2024 15:25:03 +0100 Subject: [PATCH 008/291] Fix unit test failures on Python 3.13 --- panda/src/collide/collisionNode_ext.cxx | 2 +- tests/showbase/test_Loader.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/panda/src/collide/collisionNode_ext.cxx b/panda/src/collide/collisionNode_ext.cxx index ae42a0d7ef..8508cf5206 100644 --- a/panda/src/collide/collisionNode_ext.cxx +++ b/panda/src/collide/collisionNode_ext.cxx @@ -29,7 +29,7 @@ get_owner() const { PyObject *strong_ref; int result = 0; if (owner != nullptr) { - PyWeakref_GetRef(owner, &strong_ref); + result = PyWeakref_GetRef(owner, &strong_ref); } if (result > 0) { return strong_ref; diff --git a/tests/showbase/test_Loader.py b/tests/showbase/test_Loader.py index fe0c433326..db8812fd1f 100644 --- a/tests/showbase/test_Loader.py +++ b/tests/showbase/test_Loader.py @@ -120,9 +120,10 @@ fnrgl = fnargle:FnargleLoader sys.path = [str(tmp_path), platstdlib, stdlib] Loader._loadedPythonFileTypes = False + loader = Loader() - # base parameter is only used for audio - loader = Loader(None) + if not Loader._loadedPythonFileTypes: + Loader._loadPythonFileTypes() assert Loader._loadedPythonFileTypes # Should be registered, not yet loaded From 899af61e2aacd8392890c008634286d25b67e0ad Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 19 Dec 2024 16:18:17 +0100 Subject: [PATCH 009/291] pgraph: Fix ModelPool return value reference count --- panda/src/pgraph/modelPool.I | 4 ++-- panda/src/pgraph/modelPool.cxx | 4 ++-- panda/src/pgraph/modelPool.h | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/panda/src/pgraph/modelPool.I b/panda/src/pgraph/modelPool.I index efa8fa99f8..ff796e5953 100644 --- a/panda/src/pgraph/modelPool.I +++ b/panda/src/pgraph/modelPool.I @@ -42,7 +42,7 @@ verify_model(const Filename &filename) { * date (and hasn't been modified in the meantime), and if not, will still * return NULL. */ -INLINE ModelRoot *ModelPool:: +INLINE PT(ModelRoot) ModelPool:: get_model(const Filename &filename, bool verify) { return get_ptr()->ns_get_model(filename, verify); } @@ -54,7 +54,7 @@ get_model(const Filename &filename, bool verify) { * is true and the file has recently changed). If the model file cannot be * found, or cannot be loaded for some reason, returns NULL. */ -INLINE ModelRoot *ModelPool:: +INLINE PT(ModelRoot) ModelPool:: load_model(const Filename &filename, const LoaderOptions &options) { return get_ptr()->ns_load_model(filename, options); } diff --git a/panda/src/pgraph/modelPool.cxx b/panda/src/pgraph/modelPool.cxx index 4985cd2b65..76452b4005 100644 --- a/panda/src/pgraph/modelPool.cxx +++ b/panda/src/pgraph/modelPool.cxx @@ -48,7 +48,7 @@ ns_has_model(const Filename &filename) { /** * The nonstatic implementation of get_model(). */ -ModelRoot *ModelPool:: +PT(ModelRoot) ModelPool:: ns_get_model(const Filename &filename, bool verify) { PT(ModelRoot) cached_model; @@ -116,7 +116,7 @@ ns_get_model(const Filename &filename, bool verify) { /** * The nonstatic implementation of load_model(). */ -ModelRoot *ModelPool:: +PT(ModelRoot) ModelPool:: ns_load_model(const Filename &filename, const LoaderOptions &options) { // First check if it's been cached under the given filename (for backward // compatibility reasons) diff --git a/panda/src/pgraph/modelPool.h b/panda/src/pgraph/modelPool.h index 8e0476c682..da9046c927 100644 --- a/panda/src/pgraph/modelPool.h +++ b/panda/src/pgraph/modelPool.h @@ -43,9 +43,9 @@ class EXPCL_PANDA_PGRAPH ModelPool { PUBLISHED: INLINE static bool has_model(const Filename &filename); INLINE static bool verify_model(const Filename &filename); - INLINE static ModelRoot *get_model(const Filename &filename, bool verify); - BLOCKING INLINE static ModelRoot *load_model(const Filename &filename, - const LoaderOptions &options = LoaderOptions()); + INLINE static PT(ModelRoot) get_model(const Filename &filename, bool verify); + BLOCKING INLINE static PT(ModelRoot) load_model(const Filename &filename, + const LoaderOptions &options = LoaderOptions()); INLINE static void add_model(const Filename &filename, ModelRoot *model); INLINE static void release_model(const Filename &filename); @@ -65,9 +65,9 @@ private: INLINE ModelPool(); bool ns_has_model(const Filename &filename); - ModelRoot *ns_get_model(const Filename &filename, bool verify); - ModelRoot *ns_load_model(const Filename &filename, - const LoaderOptions &options); + PT(ModelRoot) ns_get_model(const Filename &filename, bool verify); + PT(ModelRoot) ns_load_model(const Filename &filename, + const LoaderOptions &options); void ns_add_model(const Filename &filename, ModelRoot *model); void ns_release_model(const Filename &filename); From 5965d5bbece170c597f273be6c9935cc1b3c9bec Mon Sep 17 00:00:00 2001 From: burningbuffer Date: Sat, 21 Dec 2024 14:53:07 -0300 Subject: [PATCH 010/291] windisplay: Prevent window resize to zero (#1705) --- panda/src/windisplay/winGraphicsWindow.cxx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/panda/src/windisplay/winGraphicsWindow.cxx b/panda/src/windisplay/winGraphicsWindow.cxx index 4c111fad5c..3c769434a5 100644 --- a/panda/src/windisplay/winGraphicsWindow.cxx +++ b/panda/src/windisplay/winGraphicsWindow.cxx @@ -1515,6 +1515,26 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { WindowProperties properties; switch (msg) { + case WM_GETMINMAXINFO: + { + MINMAXINFO* minmaxinfo = (MINMAXINFO*)lparam; + + int minClientWidth = 1; // Minimum client area width + int minClientHeight = 1; // Minimum client area height + + // Adjust window for non-client area + RECT rect = { 0, 0, minClientWidth, minClientHeight }; + AdjustWindowRect(&rect, GetWindowLong(hwnd, GWL_STYLE), FALSE); + + // Calculate final size + int minWidth = rect.right - rect.left; + int minHeight = rect.bottom - rect.top; + + // Set the minimum track size in MINMAXINFO + minmaxinfo->ptMinTrackSize.x = minWidth; // Minimum window width + minmaxinfo->ptMinTrackSize.y = minHeight; // Minimum window height + } + break; case WM_MOUSEMOVE: if (!_tracking_mouse_leaving) { // need to re-call TrackMouseEvent every time mouse re-enters window From 598a94abfc0a5e20f2e00a2ce637ad36912e43f4 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 26 Dec 2024 15:00:55 +0100 Subject: [PATCH 011/291] android: Set minimum to API level 21 [skip ci] --- README.md | 4 ++-- makepanda/makepanda.py | 10 ++++++---- makepanda/makepandacore.py | 12 ++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6e4e78784b..0299786881 100644 --- a/README.md +++ b/README.md @@ -194,9 +194,9 @@ These commands show how to compile wheels for the supported Android ABIs: ```bash export ANDROID_SDK_ROOT=/home/rdb/local/android python3.8 makepanda/makepanda.py --everything --outputdir built-droid-arm64 --arch arm64 --target android-21 --threads 6 --wheel -python3.8 makepanda/makepanda.py --everything --outputdir built-droid-armv7a --arch armv7a --target android-19 --threads 6 --wheel +python3.8 makepanda/makepanda.py --everything --outputdir built-droid-armv7a --arch arm --target android-21 --threads 6 --wheel python3.8 makepanda/makepanda.py --everything --outputdir built-droid-x86_64 --arch x86_64 --target android-21 --threads 6 --wheel -python3.8 makepanda/makepanda.py --everything --outputdir built-droid-x86 --arch x86 --target android-19 --threads 6 --wheel +python3.8 makepanda/makepanda.py --everything --outputdir built-droid-x86 --arch x86 --target android-21 --threads 6 --wheel ``` It is now possible to use the generated wheels with `build_apps`, as explained diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 0faf692e58..f04bb08f1c 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -485,6 +485,8 @@ elif not CrossCompiling(): else: if target_arch == 'amd64': target_arch = 'x86_64' + if target_arch == 'arm' and target == 'android': + target_arch = 'armv7a' PLATFORM = '{0}-{1}'.format(target, target_arch) @@ -1371,10 +1373,10 @@ def CompileCxx(obj,src,opts): cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/') cmd += ' -ffunction-sections -funwind-tables' cmd += ' -target ' + SDK["ANDROID_TRIPLE"] - if arch == 'armv7a': + if arch in ('armv7a', 'arm'): cmd += ' -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16' - elif arch == 'arm': - cmd += ' -march=armv5te -mtune=xscale -msoft-float' + #elif arch == 'arm': + # cmd += ' -march=armv5te -mtune=xscale -msoft-float' elif arch == 'mips': cmd += ' -mips32' elif arch == 'mips64': @@ -1897,7 +1899,7 @@ def CompileLink(dll, obj, opts): cmd += ' -gcc-toolchain ' + SDK["ANDROID_GCC_TOOLCHAIN"].replace('\\', '/') cmd += " -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now" cmd += ' -target ' + SDK["ANDROID_TRIPLE"] - if arch == 'armv7a': + if arch in ('armv7a', 'arm'): cmd += " -march=armv7-a -Wl,--fix-cortex-a8" elif arch == 'mips': cmd += ' -mips32' diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index edafc82454..235385f857 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -357,11 +357,11 @@ def SetTarget(target, arch=None): elif target == 'android' or target.startswith('android-'): if arch is None: - # If compiling on Android, default to same architecture. Otherwise, arm. + # If compiling on Android, default to same architecture. if host == 'android': arch = host_arch else: - arch = 'armv7a' + exit('Specify an Android architecture using --arch') if arch == 'aarch64': arch = 'arm64' @@ -371,12 +371,9 @@ def SetTarget(target, arch=None): target, _, api = target.partition('-') if api: ANDROID_API = int(api) - elif arch in ('mips64', 'arm64', 'x86_64'): - # 64-bit platforms were introduced in Android 21. - ANDROID_API = 21 else: # Default to the lowest API level still supported by Google. - ANDROID_API = 19 + ANDROID_API = 21 # Determine the prefix for our gcc tools, eg. arm-linux-androideabi-gcc global ANDROID_ABI, ANDROID_TRIPLE @@ -1422,6 +1419,9 @@ def GetThirdpartyDir(): elif (target == 'android'): THIRDPARTYDIR = base + "/android-libs-%s/" % (target_arch) + if target_arch == 'armv7a' and not os.path.isdir(THIRDPARTYDIR): + THIRDPARTYDIR = base + "/android-libs-arm/" + elif (target == 'emscripten'): THIRDPARTYDIR = base + "/emscripten-libs/" From f4337338c149b2fa3f1c0a1662b9104f14196288 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 26 Dec 2024 15:03:19 +0100 Subject: [PATCH 012/291] readme: Update Android build instructions [skip ci] --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0299786881..881b30fc5b 100644 --- a/README.md +++ b/README.md @@ -181,22 +181,21 @@ Although it's possible to build Panda3D on an Android device using the [termux](https://termux.com/) shell, the recommended route is to cross-compile .whl files using the SDK and NDK, which can then be used by the `build_apps` command to build a Python application into an .apk or .aab bundle. You will -need to get the latest thirdparty packages, which can be obtained from the -artifacts page of the last successful run here: +need to get the latest thirdparty packages, which can be obtained from here: -https://github.com/rdb/panda3d-thirdparty/actions?query=branch%3Amain+is%3Asuccess+event%3Apush +https://rdb.name/thirdparty-android.tar.gz -This does not include Python at the moment, which can be extracted from -[this archive](https://rdb.name/thirdparty-android.tar.gz) instead. +This includes a copy of Python 3.13 compiled for Android. You will need to +use Python 3.13 on the host as well. These commands show how to compile wheels for the supported Android ABIs: ```bash export ANDROID_SDK_ROOT=/home/rdb/local/android -python3.8 makepanda/makepanda.py --everything --outputdir built-droid-arm64 --arch arm64 --target android-21 --threads 6 --wheel -python3.8 makepanda/makepanda.py --everything --outputdir built-droid-armv7a --arch arm --target android-21 --threads 6 --wheel -python3.8 makepanda/makepanda.py --everything --outputdir built-droid-x86_64 --arch x86_64 --target android-21 --threads 6 --wheel -python3.8 makepanda/makepanda.py --everything --outputdir built-droid-x86 --arch x86 --target android-21 --threads 6 --wheel +python3.13 makepanda/makepanda.py --everything --outputdir built-droid-arm64 --arch arm64 --target android-21 --threads 6 --wheel +python3.13 makepanda/makepanda.py --everything --outputdir built-droid-armv7a --arch arm --target android-21 --threads 6 --wheel +python3.13 makepanda/makepanda.py --everything --outputdir built-droid-x86_64 --arch x86_64 --target android-21 --threads 6 --wheel +python3.13 makepanda/makepanda.py --everything --outputdir built-droid-x86 --arch x86 --target android-21 --threads 6 --wheel ``` It is now possible to use the generated wheels with `build_apps`, as explained From 770e2fff0c6206bf4798c03288cb7490183343a3 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 26 Dec 2024 22:13:06 +0100 Subject: [PATCH 013/291] windisplay: Remove unused config.prc variable definitions: - responsive-minimized-fullscreen-window - hold-keys-across-windows - swapbuffer-framelock --- panda/src/windisplay/config_windisplay.cxx | 15 --------------- panda/src/windisplay/config_windisplay.h | 4 ---- 2 files changed, 19 deletions(-) diff --git a/panda/src/windisplay/config_windisplay.cxx b/panda/src/windisplay/config_windisplay.cxx index 6d655d905b..0e3090257c 100644 --- a/panda/src/windisplay/config_windisplay.cxx +++ b/panda/src/windisplay/config_windisplay.cxx @@ -27,17 +27,6 @@ ConfigureFn(config_windisplay) { init_libwindisplay(); } -ConfigVariableBool responsive_minimized_fullscreen_window -("responsive-minimized-fullscreen-window",false); - -ConfigVariableBool hold_keys_across_windows -("hold-keys-across-windows", false, - PRC_DESC("Set this true to remember the current state of the keyboard while " - "the window focus is lost, or false to pretend the user is not " - "holding down any keys while the window focus is lost. In either " - "case it should accurately restore the correct keyboard state when " - "the window focus is regained.")); - ConfigVariableBool do_vidmemsize_check ("do-vidmemsize-check", true, PRC_DESC("if true, use ddraw's GetAvailVidMem to fail if driver says " @@ -76,10 +65,6 @@ ConfigVariableBool dpi_window_resize "panel. Only available in Windows 8.1 and later, and requires " "dpi-aware to be set as well.")); -ConfigVariableBool swapbuffer_framelock -("swapbuffer-framelock", false, - PRC_DESC("Set this true to enable HW swapbuffer frame-lock on 3dlabs cards")); - ConfigVariableBool paste_emit_keystrokes ("paste-emit-keystrokes", true, PRC_DESC("Handle paste events (Ctrl-V) as separate keystroke events for each " diff --git a/panda/src/windisplay/config_windisplay.h b/panda/src/windisplay/config_windisplay.h index 377b272472..9ea1e23cc3 100644 --- a/panda/src/windisplay/config_windisplay.h +++ b/panda/src/windisplay/config_windisplay.h @@ -21,8 +21,6 @@ NotifyCategoryDecl(windisplay, EXPCL_PANDAWIN, EXPTP_PANDAWIN); -extern ConfigVariableBool responsive_minimized_fullscreen_window; -extern ConfigVariableBool hold_keys_across_windows; extern ConfigVariableBool do_vidmemsize_check; extern ConfigVariableBool auto_cpu_data; extern ConfigVariableBool ime_hide; @@ -32,8 +30,6 @@ extern ConfigVariableBool dpi_window_resize; extern ConfigVariableBool paste_emit_keystrokes; extern ConfigVariableBool disable_message_loop; -extern EXPCL_PANDAWIN ConfigVariableBool swapbuffer_framelock; - extern EXPCL_PANDAWIN void init_libwindisplay(); #endif From be382a8f29f81c289be818760eae12523aa26837 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 29 Dec 2024 14:39:43 +0100 Subject: [PATCH 014/291] makewheel: Exclude some more system libs [skip ci] --- makepanda/makewheel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/makepanda/makewheel.py b/makepanda/makewheel.py index 1ccbc618b0..f88e1fd7e3 100644 --- a/makepanda/makewheel.py +++ b/makepanda/makewheel.py @@ -88,7 +88,9 @@ MANYLINUX_LIBS = [ # These are not mentioned in manylinux1 spec but should nonetheless always # be excluded. "linux-vdso.so.1", "linux-gate.so.1", "ld-linux.so.2", "libdrm.so.2", + "ld-linux-x86-64.so.2", "ld-linux-aarch64.so.1", "libEGL.so.1", "libOpenGL.so.0", "libGLX.so.0", "libGLdispatch.so.0", + "libGLESv2.so.2", ] # Binaries to never scan for dependencies on non-Windows systems. From cc6646a448fcce061d5e89cba3cab6420d9efbae Mon Sep 17 00:00:00 2001 From: Disyer Date: Sat, 11 Jan 2025 21:32:52 +0200 Subject: [PATCH 015/291] cull: Fix crash caused by freeing a cull object with empty volume --- panda/src/cull/cullBinBackToFront.cxx | 2 +- panda/src/cull/cullBinFrontToBack.cxx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/panda/src/cull/cullBinBackToFront.cxx b/panda/src/cull/cullBinBackToFront.cxx index a8fa169304..0254c4dfdd 100644 --- a/panda/src/cull/cullBinBackToFront.cxx +++ b/panda/src/cull/cullBinBackToFront.cxx @@ -40,7 +40,7 @@ add_object(CullableObject *object, Thread *current_thread) { // Determine the center of the bounding volume. CPT(BoundingVolume) volume = object->_geom->get_bounds(current_thread); if (volume->is_empty()) { - delete object; + // No point in culling objects with no volume. return; } diff --git a/panda/src/cull/cullBinFrontToBack.cxx b/panda/src/cull/cullBinFrontToBack.cxx index 146eaaab58..dfc85dd540 100644 --- a/panda/src/cull/cullBinFrontToBack.cxx +++ b/panda/src/cull/cullBinFrontToBack.cxx @@ -40,7 +40,7 @@ add_object(CullableObject *object, Thread *current_thread) { // Determine the center of the bounding volume. CPT(BoundingVolume) volume = object->_geom->get_bounds(); if (volume->is_empty()) { - delete object; + // No point in culling objects with no volume. return; } From eb5366b9913898a1a8d596f05e0cf9d809302264 Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Thu, 16 Jan 2025 08:04:09 -0700 Subject: [PATCH 016/291] tests: Update mypy to version 1.14.1 (#1715) --- .github/workflows/mypy.yml | 4 ++-- direct/src/distributed/DistributedObjectAI.py | 4 ++-- direct/src/distributed/DistributedObjectUD.py | 2 +- direct/src/task/Task.py | 2 +- mypy.ini | 3 +++ 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 6e70577204..377ab8ffa0 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -7,7 +7,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.8', '3.11'] + python-version: ['3.9', '3.13'] fail-fast: false steps: - uses: actions/checkout@v4 @@ -18,6 +18,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install mypy==1.4.0 + pip install mypy==1.14.1 - name: Run mypy on direct run: python tests/run_mypy.py diff --git a/direct/src/distributed/DistributedObjectAI.py b/direct/src/distributed/DistributedObjectAI.py index b67ee57d1e..164d57eb2c 100644 --- a/direct/src/distributed/DistributedObjectAI.py +++ b/direct/src/distributed/DistributedObjectAI.py @@ -299,7 +299,7 @@ class DistributedObjectAI(DistributedObjectBase): # setLocation destroys self._zoneData if we move away to # a different zone if self._zoneData is None: - from otp.ai.AIZoneData import AIZoneData # type: ignore[import] + from otp.ai.AIZoneData import AIZoneData # type: ignore[import-not-found] self._zoneData = AIZoneData(self.air, self.parentId, self.zoneId) return self._zoneData @@ -489,7 +489,7 @@ class DistributedObjectAI(DistributedObjectBase): # simultaneously on different lists of avatars, although they # should have different names. - from otp.ai import Barrier # type: ignore[import] + from otp.ai import Barrier # type: ignore[import-not-found] context = self.__nextBarrierContext # We assume the context number is passed as a uint16. self.__nextBarrierContext = (self.__nextBarrierContext + 1) & 0xffff diff --git a/direct/src/distributed/DistributedObjectUD.py b/direct/src/distributed/DistributedObjectUD.py index 341ce41616..a49110e472 100755 --- a/direct/src/distributed/DistributedObjectUD.py +++ b/direct/src/distributed/DistributedObjectUD.py @@ -424,7 +424,7 @@ class DistributedObjectUD(DistributedObjectBase): # simultaneously on different lists of avatars, although they # should have different names. - from otp.ai import Barrier # type: ignore[import] + from otp.ai import Barrier # type: ignore[import-not-found] context = self.__nextBarrierContext # We assume the context number is passed as a uint16. self.__nextBarrierContext = (self.__nextBarrierContext + 1) & 0xffff diff --git a/direct/src/task/Task.py b/direct/src/task/Task.py index 9896b9c5ee..f879cbd542 100644 --- a/direct/src/task/Task.py +++ b/direct/src/task/Task.py @@ -28,7 +28,7 @@ if hasattr(sys, 'getandroidapilevel'): signal = None else: try: - import _signal as signal # type: ignore[import, no-redef] + import _signal as signal # type: ignore[import-not-found, no-redef] except ImportError: signal = None diff --git a/mypy.ini b/mypy.ini index 85a2d94031..696e4894b0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -11,3 +11,6 @@ ignore_missing_imports = True [mypy-Pmw.*] ignore_missing_imports = True + +[mypy-imp] +ignore_missing_imports = True From 271fa9d9c03afc03943db3cb1ff26a28140c6d38 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 19 Jan 2025 11:09:47 +0100 Subject: [PATCH 017/291] glgsg: Fix issues with render-to-texture and cube map arrays --- panda/src/glstuff/glGraphicsBuffer_src.cxx | 13 ++++++++----- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/panda/src/glstuff/glGraphicsBuffer_src.cxx b/panda/src/glstuff/glGraphicsBuffer_src.cxx index cfe143214d..dde857eb17 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.cxx +++ b/panda/src/glstuff/glGraphicsBuffer_src.cxx @@ -459,15 +459,17 @@ rebuild_bitplanes() { } // If we can't bind this type of texture, punt it. - if ((tex->get_texture_type() != Texture::TT_2d_texture) && - (tex->get_texture_type() != Texture::TT_3d_texture) && - (tex->get_texture_type() != Texture::TT_2d_texture_array) && - (tex->get_texture_type() != Texture::TT_cube_map)) { + Texture::TextureType texture_type = tex->get_texture_type(); + if (texture_type != Texture::TT_2d_texture && + texture_type != Texture::TT_3d_texture && + texture_type != Texture::TT_2d_texture_array && + texture_type != Texture::TT_cube_map && + texture_type != Texture::TT_cube_map_array) { ((CData *)cdata.p())->_textures[i]._rtm_mode = RTM_copy_texture; continue; } - if (_rb_size_z > 1 && tex->get_texture_type() == Texture::TT_2d_texture) { + if (_rb_size_z > 1 && texture_type == Texture::TT_2d_texture) { // We can't bind a 2D texture to a layered FBO. If the user happened // to request RTM_bind_layered for a 2D texture, that's just silly, // and we can't render to anything but the first layer anyway. @@ -1408,6 +1410,7 @@ attach_tex(GLenum attachpoint, CLP(TextureContext) *gtc, int view, int layer) { target, index, 0, layer); break; case GL_TEXTURE_2D_ARRAY: + case GL_TEXTURE_CUBE_MAP_ARRAY: glgsg->_glFramebufferTextureLayer(GL_FRAMEBUFFER_EXT, attachpoint, index, 0, layer); break; diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 1b3326a78c..21383cc825 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -7708,6 +7708,11 @@ framebuffer_copy_to_texture(Texture *tex, int view, int z, return false; } + } else if (tex->get_texture_type() == Texture::TT_cube_map_array) { + if (!_supports_cube_map_array) { + return false; + } + } else { GLCAT.error() << "Don't know how to copy framebuffer to texture " << *tex << "\n"; From db692edfa9fc6f0bb808891d772d637dfbc89c1e Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 20 Jan 2025 20:57:51 +0100 Subject: [PATCH 018/291] glgsg: Issue memory barriers for SSBOs At the moment, it assumes that all SSBO accesses are writes, once shaderpipeline lands we can sort out readonly accesses based on the qualifiers in the shader --- panda/src/gles2gsg/gles2gsg.h | 2 ++ panda/src/glstuff/glBufferContext_src.h | 4 +++ .../glstuff/glGraphicsStateGuardian_src.cxx | 20 +++++++++-- .../src/glstuff/glGraphicsStateGuardian_src.h | 3 +- panda/src/glstuff/glShaderContext_src.cxx | 34 ++++++++++++++++++- panda/src/glstuff/glShaderContext_src.h | 2 ++ 6 files changed, 61 insertions(+), 4 deletions(-) diff --git a/panda/src/gles2gsg/gles2gsg.h b/panda/src/gles2gsg/gles2gsg.h index 967f341703..c43beded29 100644 --- a/panda/src/gles2gsg/gles2gsg.h +++ b/panda/src/gles2gsg/gles2gsg.h @@ -147,6 +147,7 @@ typedef char GLchar; #define GL_FRAMEBUFFER_BARRIER_BIT 0x400 #define GL_TRANSFORM_FEEDBACK_BARRIER_BIT 0x800 #define GL_ATOMIC_COUNTER_BARRIER_BIT 0x1000 +#define GL_SHADER_STORAGE_BARRIER_BIT 0x2000 #define GL_HALF_FLOAT 0x140B #define GL_COLOR 0x1800 #define GL_DEPTH 0x1801 @@ -181,6 +182,7 @@ typedef char GLchar; #define GL_WRITE_ONLY 0x88B9 #define GL_READ_WRITE 0x88BA #define GL_PIXEL_PACK_BUFFER 0x88EB +#define GL_PIXEL_UNPACK_BUFFER 0x88EC #define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF #define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35 #define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36 diff --git a/panda/src/glstuff/glBufferContext_src.h b/panda/src/glstuff/glBufferContext_src.h index b67a9d214e..dcc31c09ab 100644 --- a/panda/src/glstuff/glBufferContext_src.h +++ b/panda/src/glstuff/glBufferContext_src.h @@ -32,6 +32,10 @@ public: // This is the GL "name" of the data object. GLuint _index; + // This is set to glgsg->_shader_storage_barrier_counter if a write was + // performed, in which case a barrier is issued before the next use. + int _shader_storage_barrier_counter = -1; + public: static TypeHandle get_class_type() { return _type_handle; diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 21383cc825..c8aa716366 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -6274,6 +6274,11 @@ issue_memory_barrier(GLbitfield barriers) { GLCAT.spam(false) << " framebuffer"; } + if (barriers & GL_SHADER_STORAGE_BARRIER_BIT) { + ++_shader_storage_barrier_counter; + GLCAT.spam(false) << " shader_storage"; + } + GLCAT.spam(false) << "\n"; report_my_gl_errors(); @@ -7367,6 +7372,9 @@ prepare_shader_buffer(ShaderBuffer *data) { _glBufferData(GL_SHADER_STORAGE_BUFFER, num_bytes, data->get_initial_data(), get_usage(data->get_usage_hint())); } + // Barrier not needed. + gbc->_shader_storage_barrier_counter = _shader_storage_barrier_counter - 1; + gbc->enqueue_lru(&_prepared_objects->_graphics_memory_lru); report_my_gl_errors(); @@ -7379,13 +7387,15 @@ prepare_shader_buffer(ShaderBuffer *data) { /** * Binds the given shader buffer to the given binding slot. */ -void CLP(GraphicsStateGuardian):: +CLP(BufferContext) *CLP(GraphicsStateGuardian):: apply_shader_buffer(GLuint base, ShaderBuffer *buffer) { + CLP(BufferContext) *gbc = nullptr; + GLuint index = 0; if (buffer != nullptr) { BufferContext *bc = buffer->prepare_now(get_prepared_objects(), this); if (bc != nullptr) { - CLP(BufferContext) *gbc = DCAST(CLP(BufferContext), bc); + gbc = DCAST(CLP(BufferContext), bc); index = gbc->_index; gbc->set_active(true); } @@ -7407,6 +7417,8 @@ apply_shader_buffer(GLuint base, ShaderBuffer *buffer) { report_my_gl_errors(); } + + return gbc; } /** @@ -7636,6 +7648,10 @@ dispatch_compute(int num_groups_x, int num_groups_y, int num_groups_z) { PStatGPUTimer timer(this, _compute_dispatch_pcollector); nassertv(_supports_compute_shaders); nassertv(_current_shader_context != nullptr); + CLP(ShaderContext) *gsc; + DCAST_INTO_V(gsc, _current_shader_context); + gsc->issue_memory_barriers(); + _glDispatchCompute(num_groups_x, num_groups_y, num_groups_z); maybe_gl_finish(); diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index 1abbc1d2a4..759ddcff68 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -391,7 +391,7 @@ public: #ifndef OPENGLES virtual BufferContext *prepare_shader_buffer(ShaderBuffer *data); - void apply_shader_buffer(GLuint base, ShaderBuffer *buffer); + CLP(BufferContext) *apply_shader_buffer(GLuint base, ShaderBuffer *buffer); virtual void release_shader_buffer(BufferContext *bc); virtual void release_shader_buffers(const pvector &contexts); #endif @@ -1167,6 +1167,7 @@ public: TextureSet _textures_needing_image_access_barrier; TextureSet _textures_needing_update_barrier; TextureSet _textures_needing_framebuffer_barrier; + int _shader_storage_barrier_counter = 0; #endif // RenderState::SlotMask _inv_state_mask; diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx index f411bb9fcc..dde88cc554 100644 --- a/panda/src/glstuff/glShaderContext_src.cxx +++ b/panda/src/glstuff/glShaderContext_src.cxx @@ -2672,6 +2672,10 @@ update_shader_vertex_arrays(ShaderContext *prev, bool force) { update_slider_table(table); } + // This ought to be moved elsewhere, but it's convenient to do this here for + // now since it's called before every Geom is drawn. + issue_memory_barriers(); + _glgsg->report_my_gl_errors(); return true; @@ -3039,7 +3043,35 @@ update_shader_buffer_bindings(ShaderContext *prev) { " (expected at least " << block._min_size << " bytes)\n"; } #endif - _glgsg->apply_shader_buffer(block._binding_index, buffer); + block._gbc = _glgsg->apply_shader_buffer(block._binding_index, buffer); + } +#endif +} + +/** + * Issues memory barriers for shader buffers, should be called before a draw. + */ +void CLP(ShaderContext):: +issue_memory_barriers() { +#ifndef OPENGLES + bool barrier_needed = false; + for (StorageBlock &block : _storage_blocks) { + if (block._gbc != nullptr && + block._gbc->_shader_storage_barrier_counter == _glgsg->_shader_storage_barrier_counter) { + barrier_needed = true; + break; + } + } + + if (barrier_needed) { + _glgsg->issue_memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT); + } + + // We assume that all SSBOs will be written to, for now. + for (StorageBlock &block : _storage_blocks) { + if (block._gbc != nullptr) { + block._gbc->_shader_storage_barrier_counter = _glgsg->_shader_storage_barrier_counter; + } } #endif } diff --git a/panda/src/glstuff/glShaderContext_src.h b/panda/src/glstuff/glShaderContext_src.h index 5da63ed21a..f196c1b6ba 100644 --- a/panda/src/glstuff/glShaderContext_src.h +++ b/panda/src/glstuff/glShaderContext_src.h @@ -58,6 +58,7 @@ public: void disable_shader_texture_bindings() override; void update_shader_texture_bindings(ShaderContext *prev) override; void update_shader_buffer_bindings(ShaderContext *prev) override; + void issue_memory_barriers(); bool uses_standard_vertex_arrays(void) override { return _uses_standard_vertex_arrays; @@ -96,6 +97,7 @@ private: #ifndef OPENGLES struct StorageBlock { CPT(InternalName) _name; + CLP(BufferContext) *_gbc = nullptr; GLuint _binding_index; GLuint _min_size; }; From 8f4d0c8a6634cbedc17ce728238145393eba85a8 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 21 Jan 2025 11:37:29 +0100 Subject: [PATCH 019/291] gobj: Make BufferContextChain thread-safe --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 1 + panda/src/gobj/bufferContext.cxx | 5 ++++- panda/src/gobj/bufferContextChain.cxx | 7 +++++++ panda/src/gobj/bufferContextChain.h | 4 ++++ panda/src/gobj/bufferResidencyTracker.cxx | 1 + 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index c8aa716366..5a06fc4afc 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -15086,6 +15086,7 @@ get_texture_memory_size(CLP(TextureContext) *gtc) { void CLP(GraphicsStateGuardian):: check_nonresident_texture(BufferContextChain &chain) { #if defined(SUPPORT_FIXED_FUNCTION) && !defined(OPENGLES) // Residency queries not supported by OpenGL ES. + LightMutexHolder holder(chain._lock); size_t num_textures = chain.get_count(); if (num_textures == 0) { return; diff --git a/panda/src/gobj/bufferContext.cxx b/panda/src/gobj/bufferContext.cxx index b268400d35..4ee4f1833d 100644 --- a/panda/src/gobj/bufferContext.cxx +++ b/panda/src/gobj/bufferContext.cxx @@ -12,6 +12,7 @@ */ #include "bufferContext.h" +#include "lightMutexHolder.h" TypeHandle BufferContext::_type_handle; @@ -43,7 +44,8 @@ BufferContext:: void BufferContext:: set_owning_chain(BufferContextChain *chain) { if (chain != _owning_chain) { - if (_owning_chain != nullptr){ + if (_owning_chain != nullptr) { + LightMutexHolder holder(_owning_chain->_lock); --(_owning_chain->_count); _owning_chain->adjust_bytes(-(int)_data_size_bytes); remove_from_list(); @@ -52,6 +54,7 @@ set_owning_chain(BufferContextChain *chain) { _owning_chain = chain; if (_owning_chain != nullptr) { + LightMutexHolder holder(_owning_chain->_lock); ++(_owning_chain->_count); _owning_chain->adjust_bytes((int)_data_size_bytes); insert_before(_owning_chain); diff --git a/panda/src/gobj/bufferContextChain.cxx b/panda/src/gobj/bufferContextChain.cxx index 4f27c90c88..780ab23350 100644 --- a/panda/src/gobj/bufferContextChain.cxx +++ b/panda/src/gobj/bufferContextChain.cxx @@ -14,11 +14,15 @@ #include "bufferContextChain.h" #include "bufferContext.h" #include "indent.h" +#include "lightMutexHolder.h" /** * Returns the first BufferContext object stored in the tracker. You can walk * through the entire list of objects stored on the tracker by calling * get_next() on each returned object, until the return value is NULL. + * + * This does not grab the lock; make sure you are holding the lock while + * iterating over the chain. */ BufferContext *BufferContextChain:: get_first() { @@ -32,9 +36,11 @@ get_first() { /** * Moves all of the BufferContexts from the other tracker onto this one. + * The other chain must be locked. */ void BufferContextChain:: take_from(BufferContextChain &other) { + LightMutexHolder holder(_lock); _total_size += other._total_size; _count += other._count; other._total_size = 0; @@ -55,6 +61,7 @@ take_from(BufferContextChain &other) { */ void BufferContextChain:: write(std::ostream &out, int indent_level) const { + LightMutexHolder holder(_lock); indent(out, indent_level) << _count << " objects, consuming " << _total_size << " bytes:\n"; diff --git a/panda/src/gobj/bufferContextChain.h b/panda/src/gobj/bufferContextChain.h index b995c2e583..153401b264 100644 --- a/panda/src/gobj/bufferContextChain.h +++ b/panda/src/gobj/bufferContextChain.h @@ -16,6 +16,7 @@ #include "pandabase.h" #include "linkedListNode.h" +#include "lightMutex.h" class BufferContext; @@ -47,6 +48,9 @@ private: size_t _total_size; int _count; +public: + LightMutex _lock; + friend class BufferContext; }; diff --git a/panda/src/gobj/bufferResidencyTracker.cxx b/panda/src/gobj/bufferResidencyTracker.cxx index 9ac4fef735..3d05fbee83 100644 --- a/panda/src/gobj/bufferResidencyTracker.cxx +++ b/panda/src/gobj/bufferResidencyTracker.cxx @@ -117,6 +117,7 @@ write(std::ostream &out, int indent_level) const { */ void BufferResidencyTracker:: move_inactive(BufferContextChain &inactive, BufferContextChain &active) { + LightMutexHolder active_holder(active._lock); BufferContext *node = active.get_first(); while (node != nullptr) { nassertv((node->_residency_state & S_active) != 0); From f872a82422e9b950b2224df5781b55af4021d0f0 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 21 Jan 2025 11:52:58 +0100 Subject: [PATCH 020/291] display: Support sharing graphics context between GraphicsEngines Automatically happens when creating a new window/buffer specifying a GSG that was created with a different engine. --- panda/src/cocoagldisplay/cocoaGLGraphicsBuffer.mm | 3 ++- panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm | 4 ++-- panda/src/cocoagldisplay/cocoaGLGraphicsWindow.mm | 3 ++- panda/src/display/graphicsEngine.cxx | 2 +- panda/src/egldisplay/eglGraphicsBuffer.cxx | 3 ++- panda/src/egldisplay/eglGraphicsPipe.cxx | 3 +++ panda/src/egldisplay/eglGraphicsWindow.cxx | 3 ++- panda/src/glxdisplay/glxGraphicsBuffer.cxx | 3 ++- panda/src/glxdisplay/glxGraphicsPipe.cxx | 3 +++ panda/src/glxdisplay/glxGraphicsWindow.cxx | 3 ++- panda/src/wgldisplay/wglGraphicsBuffer.cxx | 7 ++++--- panda/src/wgldisplay/wglGraphicsPipe.cxx | 3 +++ panda/src/wgldisplay/wglGraphicsWindow.cxx | 3 ++- 13 files changed, 30 insertions(+), 13 deletions(-) diff --git a/panda/src/cocoagldisplay/cocoaGLGraphicsBuffer.mm b/panda/src/cocoagldisplay/cocoaGLGraphicsBuffer.mm index 3632becb0b..95e4e805a3 100644 --- a/panda/src/cocoagldisplay/cocoaGLGraphicsBuffer.mm +++ b/panda/src/cocoagldisplay/cocoaGLGraphicsBuffer.mm @@ -98,7 +98,8 @@ open_buffer() { // If the old gsg has the wrong pixel format, create a new one that shares // with the old gsg. DCAST_INTO_R(cocoagsg, _gsg, false); - if (!cocoagsg->get_fb_properties().subsumes(_fb_properties)) { + if (cocoagsg->get_engine() != _engine || + !cocoagsg->get_fb_properties().subsumes(_fb_properties)) { cocoagsg = new CocoaGLGraphicsStateGuardian(_engine, _pipe, cocoagsg); cocoagsg->choose_pixel_format(_fb_properties, cocoa_pipe->get_display_id(), false); _gsg = cocoagsg; diff --git a/panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm b/panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm index 7ad1317b0b..f5ce0fff7d 100644 --- a/panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm +++ b/panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm @@ -125,12 +125,12 @@ make_output(const std::string &name, precertify = true; } } - if (host != nullptr) { + if (host != nullptr && host->get_engine() == _engine) { return new GLGraphicsBuffer(engine, this, name, fb_prop, win_prop, flags, gsg, host); } else { return new CocoaGLGraphicsBuffer(engine, this, name, fb_prop, win_prop, - flags, gsg, host); + flags, gsg, nullptr); } } diff --git a/panda/src/cocoagldisplay/cocoaGLGraphicsWindow.mm b/panda/src/cocoagldisplay/cocoaGLGraphicsWindow.mm index 33a97a90f9..71a3f87ad3 100644 --- a/panda/src/cocoagldisplay/cocoaGLGraphicsWindow.mm +++ b/panda/src/cocoagldisplay/cocoaGLGraphicsWindow.mm @@ -200,7 +200,8 @@ open_window() { // If the old gsg has the wrong pixel format, create a new one that shares // with the old gsg. DCAST_INTO_R(cocoagsg, _gsg, false); - if (!cocoagsg->get_fb_properties().subsumes(_fb_properties)) { + if (cocoagsg->get_engine() != _engine || + !cocoagsg->get_fb_properties().subsumes(_fb_properties)) { cocoagsg = new CocoaGLGraphicsStateGuardian(_engine, _pipe, cocoagsg); cocoagsg->choose_pixel_format(_fb_properties, _display, false); _gsg = cocoagsg; diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx index eb346a2b7d..3bf1b145e0 100644 --- a/panda/src/display/graphicsEngine.cxx +++ b/panda/src/display/graphicsEngine.cxx @@ -339,7 +339,7 @@ make_output(GraphicsPipe *pipe, nassertr(pipe != nullptr, nullptr); if (gsg != nullptr) { nassertr(pipe == gsg->get_pipe(), nullptr); - nassertr(this == gsg->get_engine(), nullptr); + //nassertr(this == gsg->get_engine(), nullptr); } // Are we really asking for a callback window? diff --git a/panda/src/egldisplay/eglGraphicsBuffer.cxx b/panda/src/egldisplay/eglGraphicsBuffer.cxx index a71f562611..b4fd714659 100644 --- a/panda/src/egldisplay/eglGraphicsBuffer.cxx +++ b/panda/src/egldisplay/eglGraphicsBuffer.cxx @@ -190,7 +190,8 @@ open_buffer() { // If the old gsg has the wrong pixel format, create a new one that shares // with the old gsg. DCAST_INTO_R(eglgsg, _gsg, false); - if (!eglgsg->get_fb_properties().subsumes(_fb_properties)) { + if (eglgsg->get_engine() != _engine || + !eglgsg->get_fb_properties().subsumes(_fb_properties)) { eglgsg = new eglGraphicsStateGuardian(_engine, _pipe, eglgsg); eglgsg->choose_pixel_format(_fb_properties, egl_pipe, false, true, false); _gsg = eglgsg; diff --git a/panda/src/egldisplay/eglGraphicsPipe.cxx b/panda/src/egldisplay/eglGraphicsPipe.cxx index aaa6ae0af7..ee03a267c0 100644 --- a/panda/src/egldisplay/eglGraphicsPipe.cxx +++ b/panda/src/egldisplay/eglGraphicsPipe.cxx @@ -288,6 +288,9 @@ make_output(const std::string &name, ((flags&BF_require_window)!=0)) { return nullptr; } + if (host->get_engine() != engine) { + return nullptr; + } // Early failure - if we are sure that this buffer WONT meet specs, we can // bail out early. if ((flags & BF_fb_props_optional) == 0) { diff --git a/panda/src/egldisplay/eglGraphicsWindow.cxx b/panda/src/egldisplay/eglGraphicsWindow.cxx index e82ea6862f..65d8812de9 100644 --- a/panda/src/egldisplay/eglGraphicsWindow.cxx +++ b/panda/src/egldisplay/eglGraphicsWindow.cxx @@ -223,7 +223,8 @@ open_window() { // If the old gsg has the wrong pixel format, create a new one that shares // with the old gsg. DCAST_INTO_R(eglgsg, _gsg, false); - if (!eglgsg->get_fb_properties().subsumes(_fb_properties)) { + if (eglgsg->get_engine() != _engine || + !eglgsg->get_fb_properties().subsumes(_fb_properties)) { eglgsg = new eglGraphicsStateGuardian(_engine, _pipe, eglgsg); eglgsg->choose_pixel_format(_fb_properties, egl_pipe, true, false, false); _gsg = eglgsg; diff --git a/panda/src/glxdisplay/glxGraphicsBuffer.cxx b/panda/src/glxdisplay/glxGraphicsBuffer.cxx index 6ede9a3e36..7a29be94f6 100644 --- a/panda/src/glxdisplay/glxGraphicsBuffer.cxx +++ b/panda/src/glxdisplay/glxGraphicsBuffer.cxx @@ -166,7 +166,8 @@ open_buffer() { // with the old gsg. DCAST_INTO_R(glxgsg, _gsg, false); - if (!glxgsg->_context_has_pbuffer || + if (glxgsg->get_engine() != _engine || + !glxgsg->_context_has_pbuffer || !glxgsg->get_fb_properties().subsumes(_fb_properties)) { // We need a new pixel format, and hence a new GSG. glxgsg = new glxGraphicsStateGuardian(_engine, _pipe, glxgsg); diff --git a/panda/src/glxdisplay/glxGraphicsPipe.cxx b/panda/src/glxdisplay/glxGraphicsPipe.cxx index e4ec8380fb..e4ed9ca2f6 100644 --- a/panda/src/glxdisplay/glxGraphicsPipe.cxx +++ b/panda/src/glxdisplay/glxGraphicsPipe.cxx @@ -130,6 +130,9 @@ make_output(const string &name, (flags & (BF_require_parasite | BF_require_window)) != 0) { return nullptr; } + if (host->get_engine() != engine) { + return nullptr; + } // Early failure - if we are sure that this buffer WONT meet specs, we can // bail out early. if ((flags & BF_fb_props_optional) == 0) { diff --git a/panda/src/glxdisplay/glxGraphicsWindow.cxx b/panda/src/glxdisplay/glxGraphicsWindow.cxx index 449d1f66a0..e882bc98fb 100644 --- a/panda/src/glxdisplay/glxGraphicsWindow.cxx +++ b/panda/src/glxdisplay/glxGraphicsWindow.cxx @@ -184,7 +184,8 @@ open_window() { // If the old gsg has the wrong pixel format, create a new one that shares // with the old gsg. DCAST_INTO_R(glxgsg, _gsg, false); - if (!glxgsg->get_fb_properties().subsumes(_fb_properties)) { + if (glxgsg->get_engine() != _engine || + !glxgsg->get_fb_properties().subsumes(_fb_properties)) { glxgsg = new glxGraphicsStateGuardian(_engine, _pipe, glxgsg); glxgsg->choose_pixel_format(_fb_properties, glx_pipe->get_display(), glx_pipe->get_screen(), false, false); _gsg = glxgsg; diff --git a/panda/src/wgldisplay/wglGraphicsBuffer.cxx b/panda/src/wgldisplay/wglGraphicsBuffer.cxx index 61edb9643c..3721dacb16 100644 --- a/panda/src/wgldisplay/wglGraphicsBuffer.cxx +++ b/panda/src/wgldisplay/wglGraphicsBuffer.cxx @@ -301,9 +301,10 @@ open_buffer() { // If the old gsg has the wrong pixel format, create a new one that shares // with the old gsg. DCAST_INTO_R(wglgsg, _gsg, false); - if ((!wglgsg->get_fb_properties().subsumes(_fb_properties))|| - (!wglgsg->get_fb_properties().is_single_buffered())|| - (!wglgsg->pfnum_supports_pbuffer())) { + if (wglgsg->get_engine() != _engine || + !wglgsg->get_fb_properties().subsumes(_fb_properties) || + !wglgsg->get_fb_properties().is_single_buffered() || + !wglgsg->pfnum_supports_pbuffer()) { wglgsg = new wglGraphicsStateGuardian(_engine, _pipe, wglgsg); wglgsg->choose_pixel_format(_fb_properties, true); _gsg = wglgsg; diff --git a/panda/src/wgldisplay/wglGraphicsPipe.cxx b/panda/src/wgldisplay/wglGraphicsPipe.cxx index 4186c35a35..2f4d1392af 100644 --- a/panda/src/wgldisplay/wglGraphicsPipe.cxx +++ b/panda/src/wgldisplay/wglGraphicsPipe.cxx @@ -148,6 +148,9 @@ make_output(const std::string &name, (flags & (BF_require_parasite | BF_require_window)) != 0) { return nullptr; } + if (host->get_engine() != engine) { + return nullptr; + } // Early failure - if we are sure that this buffer WONT meet specs, we can // bail out early. if ((flags & BF_fb_props_optional) == 0) { diff --git a/panda/src/wgldisplay/wglGraphicsWindow.cxx b/panda/src/wgldisplay/wglGraphicsWindow.cxx index c79ea4eb9f..536b5ad2f6 100644 --- a/panda/src/wgldisplay/wglGraphicsWindow.cxx +++ b/panda/src/wgldisplay/wglGraphicsWindow.cxx @@ -209,7 +209,8 @@ open_window() { // If the old gsg has the wrong pixel format, create a new one that shares // with the old gsg. DCAST_INTO_R(wglgsg, _gsg, false); - if (!wglgsg->get_fb_properties().subsumes(_fb_properties)) { + if (wglgsg->get_engine() != _engine || + !wglgsg->get_fb_properties().subsumes(_fb_properties)) { wglgsg = new wglGraphicsStateGuardian(_engine, _pipe, wglgsg); wglgsg->choose_pixel_format(_fb_properties, false); _gsg = wglgsg; From 8c9ba6dc38290c990d240be20d545fa72587c78d Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 21 Jan 2025 12:04:01 +0100 Subject: [PATCH 021/291] cocoa: Fix compilation error --- panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm b/panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm index f5ce0fff7d..4864ae2f8b 100644 --- a/panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm +++ b/panda/src/cocoagldisplay/cocoaGLGraphicsPipe.mm @@ -125,7 +125,7 @@ make_output(const std::string &name, precertify = true; } } - if (host != nullptr && host->get_engine() == _engine) { + if (host != nullptr && host->get_engine() == engine) { return new GLGraphicsBuffer(engine, this, name, fb_prop, win_prop, flags, gsg, host); } else { From 91f124e2f1f81c41f7aefaf3bde9dbcbd802e77f Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 21 Jan 2025 12:16:09 +0100 Subject: [PATCH 022/291] display: Allow setting a custom clock for a GraphicsEngine This is essential for supporting multiple engines, otherwise both engines will try to tick the global clock in render_frame() --- panda/src/display/graphicsEngine.cxx | 11 +++++------ panda/src/display/graphicsEngine.h | 5 ++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx index 3bf1b145e0..552be9c60a 100644 --- a/panda/src/display/graphicsEngine.cxx +++ b/panda/src/display/graphicsEngine.cxx @@ -152,8 +152,9 @@ INLINE static bool operator < (const CullKey &a, const CullKey &b) { * any Pipeline you choose. */ GraphicsEngine:: -GraphicsEngine(Pipeline *pipeline) : +GraphicsEngine(ClockObject *clock, Pipeline *pipeline) : _pipeline(pipeline), + _clock(clock), _app("app"), _lock("GraphicsEngine::_lock"), _loaded_textures_lock("GraphicsEngine::_loaded_textures_lock") @@ -729,11 +730,9 @@ render_frame() { // been rendered). open_windows(); - ClockObject *global_clock = ClockObject::get_global_clock(); - if (display_cat.is_spam()) { display_cat.spam() - << "render_frame() - frame " << global_clock->get_frame_count() << "\n"; + << "render_frame() - frame " << _clock->get_frame_count() << "\n"; } { @@ -846,8 +845,8 @@ render_frame() { } #endif // THREADED_PIPELINE - global_clock->tick(current_thread); - if (global_clock->check_errors(current_thread)) { + _clock->tick(current_thread); + if (_clock->check_errors(current_thread)) { throw_event("clock_error"); } diff --git a/panda/src/display/graphicsEngine.h b/panda/src/display/graphicsEngine.h index f0ebeb5a66..d33c245c7f 100644 --- a/panda/src/display/graphicsEngine.h +++ b/panda/src/display/graphicsEngine.h @@ -33,6 +33,7 @@ #include "loader.h" #include "referenceCount.h" #include "renderState.h" +#include "clockObject.h" class Pipeline; class DisplayRegion; @@ -53,7 +54,8 @@ class Texture; */ class EXPCL_PANDA_DISPLAY GraphicsEngine : public ReferenceCount { PUBLISHED: - explicit GraphicsEngine(Pipeline *pipeline = nullptr); + explicit GraphicsEngine(ClockObject *clock = ClockObject::get_global_clock(), + Pipeline *pipeline = nullptr); BLOCKING ~GraphicsEngine(); void set_threading_model(const GraphicsThreadingModel &threading_model); @@ -320,6 +322,7 @@ private: WindowRenderer *get_window_renderer(const std::string &name, int pipeline_stage); Pipeline *_pipeline; + ClockObject *const _clock; Windows _windows; bool _windows_sorted; From 674fa4480b8360eb8b8f8ed3124e25300e5506a8 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 22 Jan 2025 22:14:01 +0100 Subject: [PATCH 023/291] task: Fix first frame on task chain missed by PStats --- panda/src/event/asyncTaskChain.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/panda/src/event/asyncTaskChain.cxx b/panda/src/event/asyncTaskChain.cxx index a854596898..9ab2093fa3 100644 --- a/panda/src/event/asyncTaskChain.cxx +++ b/panda/src/event/asyncTaskChain.cxx @@ -1438,6 +1438,9 @@ AsyncTaskChainThread(const string &name, AsyncTaskChain *chain) : void AsyncTaskChain::AsyncTaskChainThread:: thread_main() { #ifdef HAVE_THREADS + // Let PStats know this thread exists. + PStatClient::thread_tick(); + MutexHolder holder(_chain->_manager->_lock); while (_chain->_state != S_shutdown && _chain->_state != S_interrupted) { thread_consider_yield(); From da4e2cad9f7b1b799db54e1c3bc2af113032fbbb Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 22 Jan 2025 22:14:24 +0100 Subject: [PATCH 024/291] express: Use monotonic clock on POSIX (has nanosecond precision) --- panda/src/express/trueClock.cxx | 35 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/panda/src/express/trueClock.cxx b/panda/src/express/trueClock.cxx index 157bac657e..986d2f3c64 100644 --- a/panda/src/express/trueClock.cxx +++ b/panda/src/express/trueClock.cxx @@ -521,6 +521,7 @@ TrueClock() { #include // for perror static long _init_sec; +static time_t _init_sec_monotonic = 0; /** * @@ -553,25 +554,13 @@ get_long_time() { */ double TrueClock:: get_short_raw_time() { - struct timeval tv; - - int result; - -#ifdef GETTIMEOFDAY_ONE_PARAM - result = gettimeofday(&tv); -#else - result = gettimeofday(&tv, nullptr); -#endif - - if (result < 0) { - // Error in gettimeofday(). - return 0.0; +#if defined(CLOCK_MONOTONIC) && !defined(__APPLE__) + struct timespec spec; + if (clock_gettime(CLOCK_MONOTONIC, &spec) == 0) { + return (double)(spec.tv_sec - _init_sec_monotonic) + (double)spec.tv_nsec / 1000000000.0; } - - // We subtract out the time at which the clock was initialized, because we - // don't care about the number of seconds all the way back to 1970, and we - // want to leave the double with as much precision as it can get. - return (double)(tv.tv_sec - _init_sec) + (double)tv.tv_usec / 1000000.0; +#endif + return get_long_time(); } /** @@ -603,6 +592,16 @@ TrueClock() { } else { _init_sec = tv.tv_sec; } + +#if defined(CLOCK_MONOTONIC) && !defined(__APPLE__) + struct timespec spec; + if (clock_gettime(CLOCK_MONOTONIC, &spec) == 0) { + _init_sec_monotonic = spec.tv_sec; + } else { + perror("clock_gettime(CLOCK_MONOTONIC)"); + _init_sec_monotonic = 0; + } +#endif } #endif From 3082d28fe81e4b1de4c93404ca343165f2a069ec Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 22 Jan 2025 22:17:56 +0100 Subject: [PATCH 025/291] pstatclient: Fix out-of-order events with thread profiling It could generate an end/start pair on a thread that is waiting due to other threads in that sync group submitting a PStats frame for the whole sync group, which would cause out-of-order events --- panda/src/pstatclient/pStatClientImpl.cxx | 25 +++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/panda/src/pstatclient/pStatClientImpl.cxx b/panda/src/pstatclient/pStatClientImpl.cxx index ed8a8d0e82..f6db443b55 100644 --- a/panda/src/pstatclient/pStatClientImpl.cxx +++ b/panda/src/pstatclient/pStatClientImpl.cxx @@ -155,10 +155,9 @@ client_connect(std::string hostname, int port) { int thread_index = current_thread->get_pstats_index(); if (thread_index >= 0) { PStatClient *client = PStatClient::get_global_pstats(); - double start = client->get_real_time(); + client->start(_wait_sleep_pcollector.get_index(), thread_index); ThreadImpl::sleep(seconds); - double stop = client->get_real_time(); - client->start_stop(_wait_sleep_pcollector.get_index(), thread_index, start, stop); + client->stop(_wait_sleep_pcollector.get_index(), thread_index); client->add_level(_cswitch_sleep_pcollector.get_index(), thread_index, 1); } else { @@ -171,10 +170,9 @@ client_connect(std::string hostname, int port) { int thread_index = current_thread->get_pstats_index(); if (thread_index >= 0) { PStatClient *client = PStatClient::get_global_pstats(); - double start = client->get_real_time(); + client->start(_wait_yield_pcollector.get_index(), thread_index); ThreadImpl::yield(); - double stop = client->get_real_time(); - client->start_stop(_wait_yield_pcollector.get_index(), thread_index, start, stop); + client->stop(_wait_yield_pcollector.get_index(), thread_index); client->add_level(_cswitch_yield_pcollector.get_index(), thread_index, 1); } else { @@ -190,10 +188,9 @@ client_connect(std::string hostname, int port) { BOOL result; if (thread_index >= 0) { PStatClient *client = PStatClient::get_global_pstats(); - double start = client->get_real_time(); + client->start(_wait_cvar_pcollector.get_index(), thread_index); result = SleepConditionVariableSRW(cvar, lock, time, flags); - double stop = client->get_real_time(); - client->start_stop(_wait_cvar_pcollector.get_index(), thread_index, start, stop); + client->stop(_wait_cvar_pcollector.get_index(), thread_index); client->add_level(_cswitch_cvar_pcollector.get_index(), thread_index, 1); } else { @@ -211,10 +208,9 @@ client_connect(std::string hostname, int port) { int result; if (thread_index >= 0) { PStatClient *client = PStatClient::get_global_pstats(); - double start = client->get_real_time(); + client->start(_wait_cvar_pcollector.get_index(), thread_index); result = pthread_cond_wait(cvar, lock); - double stop = client->get_real_time(); - client->start_stop(_wait_cvar_pcollector.get_index(), thread_index, start, stop); + client->stop(_wait_cvar_pcollector.get_index(), thread_index); client->add_level(_cswitch_cvar_pcollector.get_index(), thread_index, 1); } else { @@ -231,10 +227,9 @@ client_connect(std::string hostname, int port) { int result; if (thread_index >= 0) { PStatClient *client = PStatClient::get_global_pstats(); - double start = client->get_real_time(); + client->start(_wait_cvar_pcollector.get_index(), thread_index); result = pthread_cond_timedwait(cvar, lock, ts); - double stop = client->get_real_time(); - client->start_stop(_wait_cvar_pcollector.get_index(), thread_index, start, stop); + client->stop(_wait_cvar_pcollector.get_index(), thread_index); client->add_level(_cswitch_cvar_pcollector.get_index(), thread_index, 1); } else { From 7ba4fe365cb42f58fc6b6f306ce188d58cbbba01 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 25 Jan 2025 18:08:29 +0100 Subject: [PATCH 026/291] display: Add gsg.engine property --- panda/src/display/graphicsStateGuardian.h | 1 + 1 file changed, 1 insertion(+) diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index 711ab5d74e..fcf648cbbf 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -114,6 +114,7 @@ PUBLISHED: GraphicsEngine *get_engine() const; INLINE const GraphicsThreadingModel &get_threading_model() const; MAKE_PROPERTY(pipe, get_pipe); + MAKE_PROPERTY(engine, get_engine); INLINE bool is_hardware() const; virtual INLINE bool prefers_triangle_strips() const; From 3833866801806568670e5fd8c100726e271b3088 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 25 Jan 2025 18:08:54 +0100 Subject: [PATCH 027/291] display: Add method to synchronously download SSBO contents --- panda/src/display/graphicsEngine.cxx | 73 ++++++++++++++++++- panda/src/display/graphicsEngine.h | 6 +- panda/src/display/graphicsStateGuardian.cxx | 12 +++ panda/src/display/graphicsStateGuardian.h | 1 + .../glstuff/glGraphicsStateGuardian_src.cxx | 35 ++++++++- .../src/glstuff/glGraphicsStateGuardian_src.h | 1 + tests/display/test_glsl_shader.py | 10 ++- 7 files changed, 133 insertions(+), 5 deletions(-) diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx index 552be9c60a..5fab08e155 100644 --- a/panda/src/display/graphicsEngine.cxx +++ b/panda/src/display/graphicsEngine.cxx @@ -1160,7 +1160,7 @@ extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg) { // Now that the draw thread is idle, signal it to do the extraction task. thread->_gsg = gsg; thread->_texture = tex; - thread->_thread_state = TS_do_extract; + thread->_thread_state = TS_do_extract_texture_data; thread->_cv_mutex.release(); thread->_cv_start.notify(); thread->_cv_mutex.acquire(); @@ -1177,6 +1177,70 @@ extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg) { } } +/** + * Asks the indicated GraphicsStateGuardian to retrieve the buffer memory + * image of the indicated ShaderBuffer and return it. + * + * This is mainly useful for debugging. It is a very slow call because it + * introduces a pipeline stall both of Panda's pipeline and the graphics + * pipeline. + * + * The return value is empty if some kind of error occurred. + */ +vector_uchar GraphicsEngine:: +extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg) { + ReMutexHolder holder(_lock); + + string draw_name = gsg->get_threading_model().get_draw_name(); + if (draw_name.empty()) { + // A single-threaded environment. No problem. + vector_uchar data; + if (!gsg->extract_shader_buffer_data(buffer, data)) { + data.clear(); + } + return data; + } + + // A multi-threaded environment. We have to wait until the draw thread + // has finished its current task. + WindowRenderer *wr = get_window_renderer(draw_name, 0); + RenderThread *thread = (RenderThread *)wr; + MutexHolder cv_holder(thread->_cv_mutex); + + while (thread->_thread_state != TS_wait) { + thread->_cv_done.wait(); + } + + // Temporarily set this so that it accesses data from the current thread. + int pipeline_stage = Thread::get_current_pipeline_stage(); + int draw_pipeline_stage = thread->get_pipeline_stage(); + thread->set_pipeline_stage(pipeline_stage); + + // Now that the draw thread is idle, signal it to do the extraction task. + vector_uchar data; + thread->_gsg = gsg; + thread->_buffer = buffer; + thread->_buffer_result = &data; + thread->_thread_state = TS_do_extract_shader_buffer_data; + thread->_cv_mutex.release(); + thread->_cv_start.notify(); + thread->_cv_mutex.acquire(); + + // Wait for it to finish the extraction. + while (thread->_thread_state != TS_wait) { + thread->_cv_done.wait(); + } + + thread->set_pipeline_stage(draw_pipeline_stage); + thread->_gsg = nullptr; + thread->_buffer = nullptr; + thread->_buffer_result = nullptr; + if (!thread->_result) { + data.clear(); + } + return data; +} + /** * Asks the indicated GraphicsStateGuardian to dispatch the compute shader in * the given ShaderAttrib using the given work group counts. This can act as @@ -2815,11 +2879,16 @@ thread_main() { } break; - case TS_do_extract: + case TS_do_extract_texture_data: nassertd(_gsg != nullptr && _texture != nullptr) break; _result = _gsg->extract_texture_data(_texture); break; + case TS_do_extract_shader_buffer_data: + nassertd(_gsg != nullptr && _texture != nullptr) break; + _result = _gsg->extract_shader_buffer_data(_buffer, *_buffer_result); + break; + case TS_do_screenshot: nassertd(_region != nullptr) break; _texture = _region->get_screenshot(); diff --git a/panda/src/display/graphicsEngine.h b/panda/src/display/graphicsEngine.h index d33c245c7f..16034a123a 100644 --- a/panda/src/display/graphicsEngine.h +++ b/panda/src/display/graphicsEngine.h @@ -113,6 +113,7 @@ PUBLISHED: BLOCKING void flip_frame(); bool extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg); + vector_uchar extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg); void dispatch_compute(const LVecBase3i &work_groups, const RenderState *state, GraphicsStateGuardian *gsg); @@ -130,7 +131,8 @@ public: TS_do_release, TS_do_windows, TS_do_compute, - TS_do_extract, + TS_do_extract_texture_data, + TS_do_extract_shader_buffer_data, TS_do_screenshot, TS_terminate, TS_done @@ -313,6 +315,8 @@ private: // These are stored for extract_texture_data and dispatch_compute. GraphicsStateGuardian *_gsg; PT(Texture) _texture; + ShaderBuffer *_buffer; + vector_uchar *_buffer_result; const RenderState *_state; DisplayRegion *_region; LVecBase3i _work_groups; diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 11bffeb4a4..9be45d34ef 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -745,6 +745,18 @@ release_shader_buffers(const pvector &contexts) { } } +/** + * This method should only be called by the GraphicsEngine. Do not call it + * directly; call GraphicsEngine::extract_texture_data() instead. + * + * This method will be called in the draw thread to download the buffer's + * current contents synchronously. + */ +bool GraphicsStateGuardian:: +extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data) { + return false; +} + /** * Begins a new occlusion query. After this call, you may call * begin_draw_primitives() and draw_triangles()/draw_whatever() repeatedly. diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index fcf648cbbf..3ed65b8e31 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -316,6 +316,7 @@ public: virtual BufferContext *prepare_shader_buffer(ShaderBuffer *data); virtual void release_shader_buffer(BufferContext *ibc); virtual void release_shader_buffers(const pvector &contexts); + virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data); virtual void begin_occlusion_query(); virtual PT(OcclusionQueryContext) end_occlusion_query(); diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 5a06fc4afc..9988da71d0 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -7367,7 +7367,11 @@ prepare_shader_buffer(ShaderBuffer *data) { // Some drivers require the buffer to be padded to 16 byte boundary. uint64_t num_bytes = (data->get_data_size_bytes() + 15u) & ~15u; if (_supports_buffer_storage) { - _glBufferStorage(GL_SHADER_STORAGE_BUFFER, num_bytes, data->get_initial_data(), 0); + GLbitfield flags = 0; + if (data->get_usage_hint() == GeomEnums::UH_client) { + flags |= GL_CLIENT_STORAGE_BIT; + } + _glBufferStorage(GL_SHADER_STORAGE_BUFFER, num_bytes, data->get_initial_data(), flags); } else { _glBufferData(GL_SHADER_STORAGE_BUFFER, num_bytes, data->get_initial_data(), get_usage(data->get_usage_hint())); } @@ -7502,6 +7506,35 @@ release_shader_buffers(const pvector &contexts) { _glDeleteBuffers(num_indices, indices); report_my_gl_errors(); } + +/** + * This method should only be called by the GraphicsEngine. Do not call it + * directly; call GraphicsEngine::extract_texture_data() instead. + * + * This method will be called in the draw thread to download the buffer's + * current contents synchronously. + */ +bool CLP(GraphicsStateGuardian):: +extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data) { + GLuint index = 0; + BufferContext *bc = buffer->prepare_now(get_prepared_objects(), this); + if (bc == nullptr || !bc->is_of_type(CLP(BufferContext)::get_class_type())) { + return false; + } + CLP(BufferContext) *gbc = DCAST(CLP(BufferContext), bc); + + data.resize(buffer->get_data_size_bytes()); + + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); + + _glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, data.size(), &data[0]); + + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + _current_sbuffer_index = 0; + report_my_gl_errors(); + + return true; +} #endif #ifndef OPENGLES diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index 759ddcff68..03f7a0097d 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -394,6 +394,7 @@ public: CLP(BufferContext) *apply_shader_buffer(GLuint base, ShaderBuffer *buffer); virtual void release_shader_buffer(BufferContext *bc); virtual void release_shader_buffers(const pvector &contexts); + virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data); #endif #ifndef OPENGLES diff --git a/tests/display/test_glsl_shader.py b/tests/display/test_glsl_shader.py index b2cf64fefe..a49c9a0468 100644 --- a/tests/display/test_glsl_shader.py +++ b/tests/display/test_glsl_shader.py @@ -299,7 +299,7 @@ def test_glsl_uimage(gsg): def test_glsl_ssbo(gsg): - from struct import pack + from struct import pack, unpack num1 = pack(' Date: Mon, 27 Jan 2025 11:52:32 +0100 Subject: [PATCH 028/291] glgsg: Fix memory barrier for extract_shader_buffer_data --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 9988da71d0..87aeb36639 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -3207,7 +3207,7 @@ reset() { _max_image_units = 0; #ifndef OPENGLES_1 #ifdef OPENGLES - if (is_at_least_gles_version(3, 1) && gl_immutable_texture_storage) { + if (is_at_least_gles_version(3, 1)) { #else if (is_at_least_gl_version(4, 2) || has_extension("GL_ARB_shader_image_load_store")) { #endif @@ -7516,7 +7516,6 @@ release_shader_buffers(const pvector &contexts) { */ bool CLP(GraphicsStateGuardian):: extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data) { - GLuint index = 0; BufferContext *bc = buffer->prepare_now(get_prepared_objects(), this); if (bc == nullptr || !bc->is_of_type(CLP(BufferContext)::get_class_type())) { return false; @@ -7525,6 +7524,10 @@ extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data) { data.resize(buffer->get_data_size_bytes()); + if (_glMemoryBarrier != nullptr) { + _glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); + } + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); _glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, data.size(), &data[0]); From 5b0cf9e053dba0762b297744b5dd3d41c3189e85 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 27 Jan 2025 12:55:26 +0100 Subject: [PATCH 029/291] glgsg: More efficient handling of texture memory barriers New system uses counters instead of sets, just setting a counter for each barrier on a texture to the same value as a counter on the GSG when a barrier is pending, and incrementing the counter on the GSG whenever that barrier is issued, is enough to track pending barriers since barriers are global --- panda/src/glstuff/glGraphicsBuffer_src.cxx | 4 +- .../glstuff/glGraphicsStateGuardian_src.cxx | 35 +++------ .../src/glstuff/glGraphicsStateGuardian_src.h | 14 ++-- panda/src/glstuff/glShaderContext_src.cxx | 18 +++-- panda/src/glstuff/glTextureContext_src.cxx | 74 +++++++++++++------ panda/src/glstuff/glTextureContext_src.h | 13 +++- 6 files changed, 92 insertions(+), 66 deletions(-) diff --git a/panda/src/glstuff/glGraphicsBuffer_src.cxx b/panda/src/glstuff/glGraphicsBuffer_src.cxx index dde857eb17..a97818448d 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.cxx +++ b/panda/src/glstuff/glGraphicsBuffer_src.cxx @@ -281,7 +281,7 @@ begin_frame(FrameMode mode, Thread *current_thread) { CLP(GraphicsStateGuardian) *glgsg = (CLP(GraphicsStateGuardian) *)_gsg.p(); for (CLP(TextureContext) *gtc : _texture_contexts) { - if (gtc->needs_barrier(GL_FRAMEBUFFER_BARRIER_BIT)) { + if (gtc->needs_barrier(GL_FRAMEBUFFER_BARRIER_BIT, true)) { glgsg->issue_memory_barrier(GL_FRAMEBUFFER_BARRIER_BIT); // If we've done it for one, we've done it for all. break; @@ -1973,7 +1973,7 @@ resolve_multisamples() { // Issue memory barriers as necessary to make sure that the texture memory // is synchronized before we blit to it. for (CLP(TextureContext) *gtc : _texture_contexts) { - if (gtc->needs_barrier(GL_FRAMEBUFFER_BARRIER_BIT)) { + if (gtc->needs_barrier(GL_FRAMEBUFFER_BARRIER_BIT, true)) { glgsg->issue_memory_barrier(GL_FRAMEBUFFER_BARRIER_BIT); // If we've done it for one, we've done it for all. break; diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 87aeb36639..139ec128a6 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -6252,25 +6252,24 @@ issue_memory_barrier(GLbitfield barriers) { _glMemoryBarrier(barriers); - // Indicate that barriers no longer need to be issued for the relevant lists - // of textures. + // Increment these counters to indicate that these barriers have been issued. if (barriers & GL_TEXTURE_FETCH_BARRIER_BIT) { - _textures_needing_fetch_barrier.clear(); + ++_texture_fetch_barrier_counter; GLCAT.spam(false) << " texture_fetch"; } if (barriers & GL_SHADER_IMAGE_ACCESS_BARRIER_BIT) { - _textures_needing_image_access_barrier.clear(); + ++_shader_image_access_barrier_counter; GLCAT.spam(false) << " shader_image_access"; } if (barriers & GL_TEXTURE_UPDATE_BARRIER_BIT) { - _textures_needing_update_barrier.clear(); + ++_texture_update_barrier_counter; GLCAT.spam(false) << " texture_update"; } if (barriers & GL_FRAMEBUFFER_BARRIER_BIT) { - _textures_needing_framebuffer_barrier.clear(); + ++_framebuffer_barrier_counter; GLCAT.spam(false) << " framebuffer"; } @@ -6445,13 +6444,6 @@ void CLP(GraphicsStateGuardian):: release_texture(TextureContext *tc) { CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc); -#ifndef OPENGLES_1 - _textures_needing_fetch_barrier.erase(gtc); - _textures_needing_image_access_barrier.erase(gtc); - _textures_needing_update_barrier.erase(gtc); - _textures_needing_framebuffer_barrier.erase(gtc); -#endif - gtc->set_num_views(0); delete gtc; } @@ -6473,13 +6465,6 @@ release_textures(const pvector &contexts) { for (TextureContext *tc : contexts) { CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc); -#ifndef OPENGLES_1 - _textures_needing_fetch_barrier.erase(gtc); - _textures_needing_image_access_barrier.erase(gtc); - _textures_needing_update_barrier.erase(gtc); - _textures_needing_framebuffer_barrier.erase(gtc); -#endif - num_indices += gtc->_num_views; if (gtc->_buffers != nullptr) { num_buffers += gtc->_num_views; @@ -7887,8 +7872,8 @@ framebuffer_copy_to_texture(Texture *tex, int view, int z, } #ifndef OPENGLES_1 - if (gtc->needs_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT)) { - // Make sure that any incoherent writes to this texture have been synced. + if (gtc->needs_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT, true)) { + // Make sure that any reads and writes to this texture have been synced. issue_memory_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT); } #endif @@ -14175,8 +14160,8 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) { #ifndef OPENGLES_1 if (needs_reload || !image.is_null()) { - // Make sure that any incoherent writes to this texture have been synced. - if (gtc->needs_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT)) { + // Make sure that any reads and writes to this texture have been synced. + if (gtc->needs_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT, true)) { issue_memory_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT); } } @@ -15174,7 +15159,7 @@ do_extract_texture_data(CLP(TextureContext) *gtc, int view) { #ifndef OPENGLES_1 // Make sure any incoherent writes to the texture have been synced. - if (gtc->needs_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT)) { + if (gtc->needs_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT, false)) { issue_memory_barrier(GL_TEXTURE_UPDATE_BARRIER_BIT); } #endif diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index 03f7a0097d..b8d1c02d30 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -1162,12 +1162,14 @@ public: #endif #ifndef OPENGLES_1 - // Stores textures for which memory bariers should be issued. - typedef pset TextureSet; - TextureSet _textures_needing_fetch_barrier; - TextureSet _textures_needing_image_access_barrier; - TextureSet _textures_needing_update_barrier; - TextureSet _textures_needing_framebuffer_barrier; + // This count increments every time the corresponding barrier is issued. + // GLTextureContext et al store copies of this counter, when a write is + // performed on a texture, it will set its counter to match the value on the + // GSG to indicate that it is out of sync and the barrier needs to be issued. + int _texture_fetch_barrier_counter = 0; + int _shader_image_access_barrier_counter = 0; + int _texture_update_barrier_counter = 0; + int _framebuffer_barrier_counter = 0; int _shader_storage_barrier_counter = 0; #endif diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx index dde88cc554..f928882d2f 100644 --- a/panda/src/glstuff/glShaderContext_src.cxx +++ b/panda/src/glstuff/glShaderContext_src.cxx @@ -2813,12 +2813,6 @@ update_shader_texture_bindings(ShaderContext *prev) { int view = _glgsg->get_current_tex_view_offset(); gl_tex = gtc->get_view_index(view); - -#ifndef OPENGLES - if (gtc->needs_barrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)) { - barriers |= GL_SHADER_IMAGE_ACCESS_BARRIER_BIT; - } -#endif } } input._writable = false; @@ -2879,7 +2873,17 @@ update_shader_texture_bindings(ShaderContext *prev) { access = GL_READ_ONLY; gl_tex = 0; } + } else { + // If no parameters were specified, we have to assume writable access. + input._writable = true; } + +#ifndef OPENGLES + if (gtc->needs_barrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT, input._writable)) { + barriers |= GL_SHADER_IMAGE_ACCESS_BARRIER_BIT; + } +#endif + _glgsg->_glBindImageTexture(i, gl_tex, bind_level, layered, bind_layer, access, gtc->_internal_format); } @@ -2969,7 +2973,7 @@ update_shader_texture_bindings(ShaderContext *prev) { #ifndef OPENGLES // If it was recently written to, we will have to issue a memory barrier // soon. - if (gtc->needs_barrier(GL_TEXTURE_FETCH_BARRIER_BIT)) { + if (gtc->needs_barrier(GL_TEXTURE_FETCH_BARRIER_BIT, false)) { barriers |= GL_TEXTURE_FETCH_BARRIER_BIT; } #endif diff --git a/panda/src/glstuff/glTextureContext_src.cxx b/panda/src/glstuff/glTextureContext_src.cxx index 7572ef8a4f..f3d3aaf18c 100644 --- a/panda/src/glstuff/glTextureContext_src.cxx +++ b/panda/src/glstuff/glTextureContext_src.cxx @@ -63,12 +63,13 @@ reset_data(GLenum target, int num_views) { #ifndef OPENGLES_1 // Mark the texture as coherent. - if (gl_enable_memory_barriers) { - _glgsg->_textures_needing_fetch_barrier.erase(this); - _glgsg->_textures_needing_image_access_barrier.erase(this); - _glgsg->_textures_needing_update_barrier.erase(this); - _glgsg->_textures_needing_framebuffer_barrier.erase(this); - } + _texture_fetch_barrier_counter = _glgsg->_texture_fetch_barrier_counter - 1; + _shader_image_read_barrier_counter = _glgsg->_shader_image_access_barrier_counter - 1; + _shader_image_write_barrier_counter = _glgsg->_shader_image_access_barrier_counter - 1; + _texture_read_barrier_counter = _glgsg->_texture_update_barrier_counter - 1; + _texture_write_barrier_counter = _glgsg->_shader_image_access_barrier_counter - 1; + _framebuffer_read_barrier_counter = _glgsg->_framebuffer_barrier_counter - 1; + _framebuffer_write_barrier_counter = _glgsg->_framebuffer_barrier_counter - 1; #endif } @@ -168,26 +169,50 @@ set_num_views(int num_views) { #ifndef OPENGLES_1 /** - * + * Returns true if the texture needs a barrier before a read or write of the + * given kind. If writing is false, only writes are synced, otherwise both + * reads and writes are synced. */ bool CLP(TextureContext):: -needs_barrier(GLbitfield barrier) { +needs_barrier(GLbitfield barrier, bool writing) { if (!gl_enable_memory_barriers) { return false; } - return (((barrier & GL_TEXTURE_FETCH_BARRIER_BIT) && - _glgsg->_textures_needing_fetch_barrier.count(this))) - || (((barrier & GL_SHADER_IMAGE_ACCESS_BARRIER_BIT) && - _glgsg->_textures_needing_image_access_barrier.count(this))) - || (((barrier & GL_TEXTURE_UPDATE_BARRIER_BIT) && - _glgsg->_textures_needing_update_barrier.count(this))) - || (((barrier & GL_FRAMEBUFFER_BARRIER_BIT) && - _glgsg->_textures_needing_framebuffer_barrier.count(this))); + if (barrier & GL_TEXTURE_FETCH_BARRIER_BIT) { + // This is always a read, so only sync RAW. + if (_glgsg->_texture_fetch_barrier_counter == _texture_fetch_barrier_counter) { + return true; + } + } + + if (barrier & GL_SHADER_IMAGE_ACCESS_BARRIER_BIT) { + // Sync WAR, WAW and RAW, but not RAR. + if ((writing && _glgsg->_shader_image_access_barrier_counter == _shader_image_read_barrier_counter) || + (_glgsg->_shader_image_access_barrier_counter == _shader_image_write_barrier_counter)) { + return true; + } + } + + if (barrier & GL_TEXTURE_UPDATE_BARRIER_BIT) { + if ((writing && _glgsg->_texture_update_barrier_counter == _texture_read_barrier_counter) || + (_glgsg->_texture_update_barrier_counter == _texture_write_barrier_counter)) { + return true; + } + } + + if (barrier & GL_FRAMEBUFFER_BARRIER_BIT) { + if ((writing && _glgsg->_framebuffer_barrier_counter == _framebuffer_read_barrier_counter) || + (_glgsg->_framebuffer_barrier_counter == _framebuffer_write_barrier_counter)) { + return true; + } + } + + return false; } /** - * Mark a texture as needing a memory barrier, since a non-coherent read or + * Mark a texture as needing a memory barrier, since an unsynchronized read or * write just happened to it. If 'wrote' is true, it was written to. */ void CLP(TextureContext):: @@ -199,16 +224,17 @@ mark_incoherent(bool wrote) { // If we only read from it, the next read operation won't need another // barrier, since it'll be reading the same data. if (wrote) { - _glgsg->_textures_needing_fetch_barrier.insert(this); + _texture_fetch_barrier_counter = _glgsg->_texture_fetch_barrier_counter; + _shader_image_write_barrier_counter = _glgsg->_shader_image_access_barrier_counter; + _texture_write_barrier_counter = _glgsg->_shader_image_access_barrier_counter; + _framebuffer_write_barrier_counter = _glgsg->_framebuffer_barrier_counter; } // We could still write to it before we read from it, so we have to always - // insert these barriers. This could be slightly optimized so that we don't - // issue a barrier between consecutive image reads, but that may not be - // worth the trouble. - _glgsg->_textures_needing_image_access_barrier.insert(this); - _glgsg->_textures_needing_update_barrier.insert(this); - _glgsg->_textures_needing_framebuffer_barrier.insert(this); + // insert these barriers. + _shader_image_read_barrier_counter = _glgsg->_shader_image_access_barrier_counter; + _texture_read_barrier_counter = _glgsg->_texture_update_barrier_counter; + _framebuffer_read_barrier_counter = _glgsg->_framebuffer_barrier_counter; } #endif // !OPENGLES_1 diff --git a/panda/src/glstuff/glTextureContext_src.h b/panda/src/glstuff/glTextureContext_src.h index c424488471..addd03fede 100644 --- a/panda/src/glstuff/glTextureContext_src.h +++ b/panda/src/glstuff/glTextureContext_src.h @@ -41,9 +41,9 @@ public: INLINE GLuint get_view_buffer(int view) const; #ifdef OPENGLES_1 - static constexpr bool needs_barrier(GLbitfield barrier) { return false; }; + static constexpr bool needs_barrier(GLbitfield barrier, bool writing) { return false; }; #else - bool needs_barrier(GLbitfield barrier); + bool needs_barrier(GLbitfield barrier, bool writing); void mark_incoherent(bool wrote); #endif @@ -78,6 +78,15 @@ public: CLP(GraphicsStateGuardian) *_glgsg; + // These are set to the equivalent counter in glgsg when a write is performed. + int _texture_fetch_barrier_counter = -1; + int _shader_image_read_barrier_counter = -1; + int _shader_image_write_barrier_counter = -1; + int _texture_read_barrier_counter = -1; + int _texture_write_barrier_counter = -1; + int _framebuffer_read_barrier_counter = -1; + int _framebuffer_write_barrier_counter = -1; + public: static TypeHandle get_class_type() { return _type_handle; From 67723ca06ac15ae6a43f7d780602fd32682e7d14 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 27 Jan 2025 13:03:36 +0100 Subject: [PATCH 030/291] display: Generalized method for running things on draw thread This is more general and elegant than manually listing all operations in a big enum --- panda/src/display/displayRegion.cxx | 4 +- panda/src/display/graphicsEngine.I | 50 ++++++ panda/src/display/graphicsEngine.cxx | 232 ++++++--------------------- panda/src/display/graphicsEngine.h | 41 +++-- 4 files changed, 125 insertions(+), 202 deletions(-) diff --git a/panda/src/display/displayRegion.cxx b/panda/src/display/displayRegion.cxx index 84c852d472..df0e7a7292 100644 --- a/panda/src/display/displayRegion.cxx +++ b/panda/src/display/displayRegion.cxx @@ -487,7 +487,9 @@ get_screenshot() { if (gsg->get_threading_model().get_draw_stage() != current_thread->get_pipeline_stage()) { // Ask the engine to do on the draw thread. GraphicsEngine *engine = window->get_engine(); - return engine->do_get_screenshot(this, gsg); + return engine->run_on_draw_thread([this] { + return get_screenshot(); + }); } // We are on the draw thread. diff --git a/panda/src/display/graphicsEngine.I b/panda/src/display/graphicsEngine.I index e9fc42ce44..ca9142a8a8 100644 --- a/panda/src/display/graphicsEngine.I +++ b/panda/src/display/graphicsEngine.I @@ -171,3 +171,53 @@ INLINE void GraphicsEngine:: dispatch_compute(const LVecBase3i &work_groups, const ShaderAttrib *sattr, GraphicsStateGuardian *gsg) { dispatch_compute(work_groups, RenderState::make(sattr), gsg); } + +#ifndef CPPPARSER +/** + * Waits for the draw thread to become idle, then runs the given function on it. + */ +template +INLINE auto GraphicsEngine:: +run_on_draw_thread(Callable &&callable) -> decltype(callable()) { + ReMutexHolder holder(_lock); + std::string draw_name = _threading_model.get_draw_name(); + if (draw_name.empty()) { + return std::move(callable)(); + } else { + WindowRenderer *wr = get_window_renderer(draw_name, 0); + RenderThread *thread = (RenderThread *)wr; + return thread->run_on_thread(std::move(callable)); + } +} + +/** + * Waits for this thread to become idle, then runs the given function on it. + */ +template +INLINE auto GraphicsEngine::RenderThread:: +run_on_thread(Callable &&callable) -> + typename std::enable_if::value, decltype(callable())>::type { + + using ReturnType = decltype(callable()); + alignas(ReturnType) unsigned char storage[sizeof(ReturnType)]; + + run_on_thread([] (RenderThread *data) { + new (data->_return_data) ReturnType(std::move(*(Callable *)data->_callback_data)()); + }, &callable, storage); + + return *(ReturnType *)storage; +} + +/** + * Waits for this thread to become idle, then runs the given function on it. + */ +template +INLINE auto GraphicsEngine::RenderThread:: +run_on_thread(Callable &&callable) -> + typename std::enable_if::value, decltype(callable())>::type { + + run_on_thread([] (RenderThread *data) { + std::move(*(Callable *)data->_callback_data)(); + }, &callable, nullptr); +} +#endif // CPPPARSER diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx index 5fab08e155..474f8e2934 100644 --- a/panda/src/display/graphicsEngine.cxx +++ b/panda/src/display/graphicsEngine.cxx @@ -1134,47 +1134,9 @@ flip_frame() { */ bool GraphicsEngine:: extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg) { - ReMutexHolder holder(_lock); - - string draw_name = gsg->get_threading_model().get_draw_name(); - if (draw_name.empty()) { - // A single-threaded environment. No problem. + return run_on_draw_thread([=] () { return gsg->extract_texture_data(tex); - - } else { - // A multi-threaded environment. We have to wait until the draw thread - // has finished its current task. - WindowRenderer *wr = get_window_renderer(draw_name, 0); - RenderThread *thread = (RenderThread *)wr; - MutexHolder cv_holder(thread->_cv_mutex); - - while (thread->_thread_state != TS_wait) { - thread->_cv_done.wait(); - } - - // Temporarily set this so that it accesses data from the current thread. - int pipeline_stage = Thread::get_current_pipeline_stage(); - int draw_pipeline_stage = thread->get_pipeline_stage(); - thread->set_pipeline_stage(pipeline_stage); - - // Now that the draw thread is idle, signal it to do the extraction task. - thread->_gsg = gsg; - thread->_texture = tex; - thread->_thread_state = TS_do_extract_texture_data; - thread->_cv_mutex.release(); - thread->_cv_start.notify(); - thread->_cv_mutex.acquire(); - - // Wait for it to finish the extraction. - while (thread->_thread_state != TS_wait) { - thread->_cv_done.wait(); - } - - thread->set_pipeline_stage(draw_pipeline_stage); - thread->_gsg = nullptr; - thread->_texture = nullptr; - return thread->_result; - } + }); } /** @@ -1189,56 +1151,13 @@ extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg) { */ vector_uchar GraphicsEngine:: extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg) { - ReMutexHolder holder(_lock); - - string draw_name = gsg->get_threading_model().get_draw_name(); - if (draw_name.empty()) { - // A single-threaded environment. No problem. + return run_on_draw_thread([=] () { vector_uchar data; if (!gsg->extract_shader_buffer_data(buffer, data)) { data.clear(); } return data; - } - - // A multi-threaded environment. We have to wait until the draw thread - // has finished its current task. - WindowRenderer *wr = get_window_renderer(draw_name, 0); - RenderThread *thread = (RenderThread *)wr; - MutexHolder cv_holder(thread->_cv_mutex); - - while (thread->_thread_state != TS_wait) { - thread->_cv_done.wait(); - } - - // Temporarily set this so that it accesses data from the current thread. - int pipeline_stage = Thread::get_current_pipeline_stage(); - int draw_pipeline_stage = thread->get_pipeline_stage(); - thread->set_pipeline_stage(pipeline_stage); - - // Now that the draw thread is idle, signal it to do the extraction task. - vector_uchar data; - thread->_gsg = gsg; - thread->_buffer = buffer; - thread->_buffer_result = &data; - thread->_thread_state = TS_do_extract_shader_buffer_data; - thread->_cv_mutex.release(); - thread->_cv_start.notify(); - thread->_cv_mutex.acquire(); - - // Wait for it to finish the extraction. - while (thread->_thread_state != TS_wait) { - thread->_cv_done.wait(); - } - - thread->set_pipeline_stage(draw_pipeline_stage); - thread->_gsg = nullptr; - thread->_buffer = nullptr; - thread->_buffer_result = nullptr; - if (!thread->_result) { - data.clear(); - } - return data; + }); } /** @@ -1263,50 +1182,12 @@ dispatch_compute(const LVecBase3i &work_groups, const RenderState *state, Graphi nassertv(shader != nullptr); nassertv(gsg != nullptr); - ReMutexHolder holder(_lock); - - string draw_name = gsg->get_threading_model().get_draw_name(); - if (draw_name.empty()) { - // A single-threaded environment. No problem. + run_on_draw_thread([=] () { gsg->push_group_marker(std::string("Compute ") + shader->get_filename(Shader::ST_compute).get_basename()); gsg->set_state_and_transform(state, TransformState::make_identity()); gsg->dispatch_compute(work_groups[0], work_groups[1], work_groups[2]); gsg->pop_group_marker(); - - } else { - // A multi-threaded environment. We have to wait until the draw thread - // has finished its current task. - WindowRenderer *wr = get_window_renderer(draw_name, 0); - RenderThread *thread = (RenderThread *)wr; - MutexHolder cv_holder(thread->_cv_mutex); - - while (thread->_thread_state != TS_wait) { - thread->_cv_done.wait(); - } - - // Temporarily set this so that it accesses data from the current thread. - int pipeline_stage = Thread::get_current_pipeline_stage(); - int draw_pipeline_stage = thread->get_pipeline_stage(); - thread->set_pipeline_stage(pipeline_stage); - - // Now that the draw thread is idle, signal it to do the compute task. - thread->_gsg = gsg; - thread->_state = state; - thread->_work_groups = work_groups; - thread->_thread_state = TS_do_compute; - thread->_cv_mutex.release(); - thread->_cv_start.notify(); - thread->_cv_mutex.acquire(); - - // Wait for it to finish the compute task. - while (thread->_thread_state != TS_wait) { - thread->_cv_done.wait(); - } - - thread->set_pipeline_stage(draw_pipeline_stage); - thread->_gsg = nullptr; - thread->_state = nullptr; - } + }); } /** @@ -1342,43 +1223,6 @@ texture_uploaded(Texture *tex) { // Usually only called by DisplayRegion::do_cull. } -/** - * Called by DisplayRegion::do_get_screenshot - */ -PT(Texture) GraphicsEngine:: -do_get_screenshot(DisplayRegion *region, GraphicsStateGuardian *gsg) { - // A multi-threaded environment. We have to wait until the draw thread - // has finished its current task. - - ReMutexHolder holder(_lock); - - const std::string &draw_name = gsg->get_threading_model().get_draw_name(); - WindowRenderer *wr = get_window_renderer(draw_name, 0); - RenderThread *thread = (RenderThread *)wr; - MutexHolder cv_holder(thread->_cv_mutex); - - while (thread->_thread_state != TS_wait) { - thread->_cv_done.wait(); - } - - // Now that the draw thread is idle, signal it to do the extraction task. - thread->_region = region; - thread->_thread_state = TS_do_screenshot; - thread->_cv_mutex.release(); - thread->_cv_start.notify(); - thread->_cv_mutex.acquire(); - - // Wait for it to finish the extraction. - while (thread->_thread_state != TS_wait) { - thread->_cv_done.wait(); - } - - PT(Texture) tex = std::move(thread->_texture); - thread->_region = nullptr; - thread->_texture = nullptr; - return tex; -} - /** * Fires off a cull traversal using the indicated camera. */ @@ -2867,31 +2711,9 @@ thread_main() { do_pending(_engine, current_thread); break; - case TS_do_compute: - nassertd(_gsg != nullptr && _state != nullptr) break; - { - const ShaderAttrib *sattr; - _state->get_attrib(sattr); - _gsg->push_group_marker(std::string("Compute ") + sattr->get_shader()->get_filename(Shader::ST_compute).get_basename()); - _gsg->set_state_and_transform(_state, TransformState::make_identity()); - _gsg->dispatch_compute(_work_groups[0], _work_groups[1], _work_groups[2]); - _gsg->pop_group_marker(); - } - break; - - case TS_do_extract_texture_data: - nassertd(_gsg != nullptr && _texture != nullptr) break; - _result = _gsg->extract_texture_data(_texture); - break; - - case TS_do_extract_shader_buffer_data: - nassertd(_gsg != nullptr && _texture != nullptr) break; - _result = _gsg->extract_shader_buffer_data(_buffer, *_buffer_result); - break; - - case TS_do_screenshot: - nassertd(_region != nullptr) break; - _texture = _region->get_screenshot(); + case TS_callback: + nassertd(_callback != nullptr) break; + _callback(this); break; case TS_terminate: @@ -2916,3 +2738,39 @@ thread_main() { } } } + +/** + * Waits for this thread to become idle, then runs the given function on it. + */ +void GraphicsEngine::RenderThread:: +run_on_thread(Callback *callback, void *callback_data, void *return_data) { + MutexHolder cv_holder(_cv_mutex); + + while (_thread_state != TS_wait) { + _cv_done.wait(); + } + + // Temporarily set this so that it accesses data from the current thread. + int pipeline_stage = Thread::get_current_pipeline_stage(); + int thread_pipeline_stage = get_pipeline_stage(); + set_pipeline_stage(pipeline_stage); + + // Now that the draw thread is idle, signal it to run the callback. + _callback = callback; + _callback_data = callback_data; + _return_data = return_data; + _thread_state = TS_callback; + _cv_mutex.release(); + _cv_start.notify(); + _cv_mutex.acquire(); + + // Wait for it to finish the job. + while (_thread_state != TS_wait) { + _cv_done.wait(); + } + + set_pipeline_stage(thread_pipeline_stage); + _callback = nullptr; + _callback_data = nullptr; + _return_data = nullptr; +} diff --git a/panda/src/display/graphicsEngine.h b/panda/src/display/graphicsEngine.h index 16034a123a..88874f8e50 100644 --- a/panda/src/display/graphicsEngine.h +++ b/panda/src/display/graphicsEngine.h @@ -35,6 +35,8 @@ #include "renderState.h" #include "clockObject.h" +#include + class Pipeline; class DisplayRegion; class GraphicsPipe; @@ -130,16 +132,17 @@ public: TS_do_flip, TS_do_release, TS_do_windows, - TS_do_compute, - TS_do_extract_texture_data, - TS_do_extract_shader_buffer_data, - TS_do_screenshot, + TS_callback, TS_terminate, TS_done }; void texture_uploaded(Texture *tex); - PT(Texture) do_get_screenshot(DisplayRegion *region, GraphicsStateGuardian *gsg); + +#ifndef CPPPARSER + template + INLINE auto run_on_draw_thread(Callable &&callable) -> decltype(callable()); +#endif public: static void do_cull(CullHandler *cull_handler, SceneSetup *scene_setup, @@ -306,21 +309,31 @@ private: RenderThread(const std::string &name, GraphicsEngine *engine); virtual void thread_main(); + typedef void Callback(RenderThread *thread); + void run_on_thread(Callback *callback, + void *callback_data = nullptr, + void *return_data = nullptr); + +#ifndef CPPPARSER + template + INLINE auto run_on_thread(Callable &&callable) -> + typename std::enable_if::value, decltype(callable())>::type; + + template + INLINE auto run_on_thread(Callable &&callable) -> + typename std::enable_if::value, decltype(callable())>::type; +#endif + GraphicsEngine *_engine; Mutex _cv_mutex; ConditionVar _cv_start; ConditionVar _cv_done; ThreadState _thread_state; - // These are stored for extract_texture_data and dispatch_compute. - GraphicsStateGuardian *_gsg; - PT(Texture) _texture; - ShaderBuffer *_buffer; - vector_uchar *_buffer_result; - const RenderState *_state; - DisplayRegion *_region; - LVecBase3i _work_groups; - bool _result; + // Used for TS_callback. + Callback *_callback; + void *_callback_data; + void *_return_data; }; WindowRenderer *get_window_renderer(const std::string &name, int pipeline_stage); From 2ea02ef0c8ad3d43e01b67f6d1ed1b2dafb46ac1 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 24 Jan 2025 22:19:42 +0100 Subject: [PATCH 031/291] putil: Add new primitives for async callbacks/futures These are a lot lighter than AsyncFuture and safer to use in C++ due to the reliance on RAII. An async method can now take a CompletionToken, which will implicitly accept an arbitrary callable or an AsyncFuture pointer. --- panda/src/event/asyncFuture.cxx | 11 +++ panda/src/event/asyncFuture.h | 33 ++++++++- panda/src/putil/CMakeLists.txt | 4 ++ panda/src/putil/completable.I | 61 ++++++++++++++++ panda/src/putil/completable.h | 79 +++++++++++++++++++++ panda/src/putil/completionCounter.I | 97 ++++++++++++++++++++++++++ panda/src/putil/completionCounter.cxx | 52 ++++++++++++++ panda/src/putil/completionCounter.h | 58 +++++++++++++++ panda/src/putil/completionToken.I | 42 +++++++++++ panda/src/putil/completionToken.h | 56 +++++++++++++++ panda/src/putil/p3putil_composite1.cxx | 1 + 11 files changed, 493 insertions(+), 1 deletion(-) create mode 100644 panda/src/putil/completable.I create mode 100644 panda/src/putil/completable.h create mode 100644 panda/src/putil/completionCounter.I create mode 100644 panda/src/putil/completionCounter.cxx create mode 100644 panda/src/putil/completionCounter.h create mode 100644 panda/src/putil/completionToken.I create mode 100644 panda/src/putil/completionToken.h diff --git a/panda/src/event/asyncFuture.cxx b/panda/src/event/asyncFuture.cxx index d8b8b91d97..a6aa341c07 100644 --- a/panda/src/event/asyncFuture.cxx +++ b/panda/src/event/asyncFuture.cxx @@ -389,6 +389,17 @@ wake_task(AsyncTask *task) { } } +/** + * Internal callback called when a CompletionToken created from this future + * completes. + */ +void AsyncFuture:: +token_callback(Completable::Data *data, bool success) { + AsyncFuture *future = (AsyncFuture *)data; + future->set_result(EventParameter(success)); + unref_delete(future); +} + /** * @see AsyncFuture::gather */ diff --git a/panda/src/event/asyncFuture.h b/panda/src/event/asyncFuture.h index acb6b1020f..f9f71472f8 100644 --- a/panda/src/event/asyncFuture.h +++ b/panda/src/event/asyncFuture.h @@ -20,6 +20,7 @@ #include "eventParameter.h" #include "patomic.h" #include "small_vector.h" +#include "completionToken.h" class AsyncTaskManager; class AsyncTask; @@ -58,7 +59,7 @@ class AsyncTask; * * @since 1.10.0 */ -class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount { +class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount, protected Completable::Data { PUBLISHED: INLINE AsyncFuture(); virtual ~AsyncFuture(); @@ -109,6 +110,8 @@ public: private: void wake_task(AsyncTask *task); + static void token_callback(Completable::Data *, bool success); + protected: enum FutureState : patomic_unsigned_lock_free::value_type { // Pending states @@ -136,6 +139,7 @@ protected: friend class AsyncGatheringFuture; friend class AsyncTaskChain; + friend class CompletionToken; friend class PythonTask; public: @@ -199,6 +203,33 @@ private: static TypeHandle _type_handle; }; +#ifndef CPPPARSER +// Allow passing a future into a method accepting a CompletionToken. +template<> +INLINE CompletionToken:: +CompletionToken(AsyncFuture *future) { + if (future != nullptr) { + future->ref(); + _callback._data = future; + if (_callback._data->_function == nullptr) { + _callback._data->_function = &AsyncFuture::token_callback; + } + } +} + +template<> +INLINE CompletionToken:: +CompletionToken(PT(AsyncFuture) future) { + if (future != nullptr) { + _callback._data = future; + if (_callback._data->_function == nullptr) { + _callback._data->_function = &AsyncFuture::token_callback; + } + future.cheat() = nullptr; + } +} +#endif + #include "asyncFuture.I" #endif // !ASYNCFUTURE_H diff --git a/panda/src/putil/CMakeLists.txt b/panda/src/putil/CMakeLists.txt index ecc14b9843..c99dfac383 100644 --- a/panda/src/putil/CMakeLists.txt +++ b/panda/src/putil/CMakeLists.txt @@ -20,6 +20,9 @@ set(P3PUTIL_HEADERS clockObject.h clockObject.I collideMask.h colorSpace.h + completable.I completable.h + completionCounter.I completionCounter.h + completionToken.I completionToken.h copyOnWriteObject.h copyOnWriteObject.I copyOnWritePointer.h copyOnWritePointer.I compareTo.I compareTo.h @@ -86,6 +89,7 @@ set(P3PUTIL_SOURCES callbackObject.cxx clockObject.cxx colorSpace.cxx + completionCounter.cxx copyOnWriteObject.cxx copyOnWritePointer.cxx config_putil.cxx configurable.cxx diff --git a/panda/src/putil/completable.I b/panda/src/putil/completable.I new file mode 100644 index 0000000000..4f44471615 --- /dev/null +++ b/panda/src/putil/completable.I @@ -0,0 +1,61 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completable.I + * @author rdb + * @date 2025-01-22 + */ + +#ifndef CPPPARSER +/** + * + */ +template +INLINE Completable:: +Completable(Callable callback) : + _data(new LambdaData(std::move(callback), [](Data *data, bool do_run) { + LambdaData *self = (LambdaData *)data; + if (do_run) { + std::move(self->_lambda)(); + } + delete self; + })) { +} +#endif + +/** + * + */ +INLINE Completable:: +Completable(Completable &&from) noexcept : + _data(from._data) { + from._data = nullptr; +} + +/** + * + */ +INLINE Completable:: +~Completable() { + Data *data = _data; + if (data != nullptr) { + data->_function.load(std::memory_order_relaxed)(data, false); + } +} + +/** + * + */ +INLINE void Completable:: +operator ()() { + Data *data = _data; + _data = nullptr; + if (data != nullptr) { + data->_function.load(std::memory_order_relaxed)(data, true); + } +} diff --git a/panda/src/putil/completable.h b/panda/src/putil/completable.h new file mode 100644 index 0000000000..a184302d9c --- /dev/null +++ b/panda/src/putil/completable.h @@ -0,0 +1,79 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completable.h + * @author rdb + * @date 2025-01-22 + */ + +#ifndef COMPLETABLE_H +#define COMPLETABLE_H + +#include "pandabase.h" +#include "patomic.h" + +/** + * Stores a type-erased callable that is move-only. May only be called once. + */ +class EXPCL_PANDA_PUTIL Completable { +public: + constexpr Completable() = default; + +#ifndef CPPPARSER + template + INLINE Completable(Callable callback); +#endif + + INLINE Completable(const Completable ©) = delete; + INLINE Completable(Completable &&from) noexcept; + + INLINE void operator ()(); + + INLINE ~Completable(); + +protected: + // There are several design approaches here: + // 1. Optimize for no data block: do not require dynamic allocation of a data + // block in the simple case where the callback data is only the size of a + // single pointer. Store two pointers, one function pointer and a data + // pointer(-sized storage), directly on the class here. + // 2. Optimize for a data block: store the function pointer on the data block, + // always requiring dynamic allocation. + // + // Right now I have opted for 2 because it allows the function pointer to be + // dynamically swapped (used in CompletionCounter), but this decision may + // change in the future. + + struct Data; + typedef void CallbackFunction(Data *, bool); + + struct Data { + patomic _function { nullptr }; + }; + + template + struct LambdaData : public Data { + // Must unfortunately be defined inline, since this struct is protected. + LambdaData(Lambda lambda, CallbackFunction *function) : + _lambda(std::move(lambda)) { + _function = function; + } + + Lambda _lambda; + }; + + Data *_data = nullptr; + + friend class AsyncFuture; + friend class CompletionCounter; + friend class CompletionToken; +}; + +#include "completable.I" + +#endif diff --git a/panda/src/putil/completionCounter.I b/panda/src/putil/completionCounter.I new file mode 100644 index 0000000000..6d59143360 --- /dev/null +++ b/panda/src/putil/completionCounter.I @@ -0,0 +1,97 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionCounter.I + * @author rdb + * @date 2025-01-22 + */ + +/** + * + */ +INLINE CompletionCounter:: +~CompletionCounter() { + CounterData *data = _data; + if (data != nullptr) { + // then() is not called; we still need something that destructs the data + // when done. + auto prev_function = data->_function.exchange(&abandon_callback, std::memory_order_relaxed); + if (prev_function == nullptr) { + // Was already done. + delete data; + } + } +} + +/** + * Returns a new token. May not be called after then(). + */ +INLINE CompletionToken CompletionCounter:: +make_token() { + CompletionToken token; + if (_data == nullptr) { + _data = new CounterData; + _data->_function = &initial_callback; + } + auto old_value = _data->_counter.fetch_add(1); + nassertr(old_value >= 0, token); + token._callback._data = _data; + return token; +} + +/** + * Runs the given callback immediately upon completion. If the counter is + * already done, runs it immediately. This requires an rvalue because it + * consumes the counter, use std::move() if you don't have an rvalue. + * + * The callback will either be called immediately or directly when the last + * token calls complete(), however, it may also be called if a token is + * destroyed. This may happen at unexpected times, such as when the lambda + * holding the token is destroyed prematurely. In this case, however, the + * passed success argument will always be false. + */ +template +INLINE void CompletionCounter:: +then(Callable callable) && { + // Replace the callback pointer with something that calls the given callable + // once the count reaches 0. + CounterData *data = _data; + nassertv(data != nullptr); + _data = nullptr; + if (data->_function.load(std::memory_order_acquire) == nullptr) { + // Already done. + callable((data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0); + delete data; + return; + } + + static_assert(sizeof(Callable) <= sizeof(data->_storage), + "raise storage size in completionCounter.h or reduce lambda captures"); + + new (data->_storage) Callable(std::move(callable)); + + Completable::CallbackFunction *new_function = + [] (Completable::Data *data_ptr, bool success) { + CounterData *data = (CounterData *)data_ptr; + auto prev_count = data->_counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release); + if ((short)(prev_count & 0xffff) > 1) { + return; + } + + Callable *callable = (Callable *)data->_storage; + std::move(*callable)(success && (prev_count & ~0xffff) == 0); + callable->~Callable(); + delete data; + }; + + auto prev_function = data->_function.exchange(new_function, std::memory_order_acq_rel); + if (UNLIKELY(prev_function == nullptr)) { + // Last token finished in the meantime. + new_function(data, (data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0); + } +} diff --git a/panda/src/putil/completionCounter.cxx b/panda/src/putil/completionCounter.cxx new file mode 100644 index 0000000000..2540867e61 --- /dev/null +++ b/panda/src/putil/completionCounter.cxx @@ -0,0 +1,52 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionCounter.cxx + * @author rdb + * @date 2025-01-24 + */ + +#include "completionCounter.h" + +/** + * Called when a token is completed before then() is called. + */ +void CompletionCounter:: +initial_callback(Completable::Data *data_ptr, bool success) { + CounterData &data = *(CounterData *)data_ptr; + auto prev_count = data._counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release); + if ((prev_count & 0xffff) == 1) { + // We're done early. + auto prev_callback = data._function.exchange(nullptr, std::memory_order_acq_rel); + nassertv(prev_callback != nullptr); + + // Someone called then() in the meantime. Call the new callback. The + // refcount will drop below 0 when that's called but they are designed to + // handle that. + if (prev_callback != &initial_callback) { + prev_callback(data_ptr, success && (prev_count & ~0xffff) == 0); + } + } +} + +/** + * Called when a token is completed after this object is destroyed without + * then() being called. + */ +void CompletionCounter:: +abandon_callback(Completable::Data *data_ptr, bool success) { + CounterData &data = *(CounterData *)data_ptr; + auto prev_count = data._counter.fetch_sub(1, std::memory_order_relaxed); + if ((prev_count & 0xffff) <= 1) { + // Done. + auto prev_callback = data._function.exchange(nullptr, std::memory_order_relaxed); + nassertv(prev_callback != nullptr); + nassertv(prev_callback == &abandon_callback); + delete &data; + } +} diff --git a/panda/src/putil/completionCounter.h b/panda/src/putil/completionCounter.h new file mode 100644 index 0000000000..dbb0e2dcfb --- /dev/null +++ b/panda/src/putil/completionCounter.h @@ -0,0 +1,58 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionCounter.h + * @author rdb + * @date 2025-01-22 + */ + +#ifndef COMPLETIONCOUNTER_H +#define COMPLETIONCOUNTER_H + +#include "pandabase.h" +#include "completionToken.h" + +#include + +/** + * Shared counter that generates "completion tokens" incrementing a counter, + * which will decrement the counter once they are finished. After the tokens + * are handed out, a callback may be registered using then(), which will be + * called as soon as the last token is done. + */ +class EXPCL_PANDA_PUTIL CompletionCounter { +public: + constexpr CompletionCounter() = default; + CompletionCounter(const CompletionCounter ©) = delete; + + INLINE ~CompletionCounter(); + + INLINE CompletionToken make_token(); + + template + INLINE void then(Callable callable) &&; + +private: + static void initial_callback(Completable::Data *data, bool success); + static void abandon_callback(Completable::Data *data, bool success); + +protected: + struct CounterData : public Completable::Data { + // Least significant half is counter, most significant half is error count + patomic_signed_lock_free _counter { 0 }; + + // Just raise this if the static_assert fires (or limit the size of your + // lambda captures). + alignas(std::max_align_t) unsigned char _storage[64]; + }; + CounterData *_data = nullptr; +}; + +#include "completionCounter.I" + +#endif diff --git a/panda/src/putil/completionToken.I b/panda/src/putil/completionToken.I new file mode 100644 index 0000000000..aef06a7d4e --- /dev/null +++ b/panda/src/putil/completionToken.I @@ -0,0 +1,42 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionToken.I + * @author rdb + * @date 2025-01-22 + */ + +#ifndef CPPPARSER +/** + * Creates a token that calls the given callback when it's done, passing it + * true on success and false on failure or abandonment. + */ +template +INLINE CompletionToken:: +CompletionToken(Callable callback) { + // Main difference over a Completable is that this will always call the + // callback, even on failure, so that cleanup can be done. + _callback._data = new Completable::LambdaData(std::move(callback), [](Completable::Data *data, bool success) { + Completable::LambdaData *self = (Completable::LambdaData *)data; + std::move(self->_lambda)(success); + delete self; + }); +} +#endif + +/** + * + */ +INLINE void CompletionToken:: +complete(bool success) { + Completable::Data *data = _callback._data; + if (data != nullptr) { + _callback._data = nullptr; + data->_function.load(std::memory_order_relaxed)(data, success); + } +} diff --git a/panda/src/putil/completionToken.h b/panda/src/putil/completionToken.h new file mode 100644 index 0000000000..b73f4fe376 --- /dev/null +++ b/panda/src/putil/completionToken.h @@ -0,0 +1,56 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file completionToken.h + * @author rdb + * @date 2025-01-22 + */ + +#ifndef COMPLETIONTOKEN_H +#define COMPLETIONTOKEN_H + +#include "pandabase.h" +#include "pnotify.h" +#include "completable.h" + +/** + * A completion token can be created from a callback, future or + * CompletionCounter and can be passed into an asynchronous operation in order + * to receive a signal when it is done. + * + * The asynchronous operation should call complete() on it when it is done, + * with a boolean value indicating success or failure. If the token is + * destroyed prematurely, it is treated as if it called complete(false). + * + * This should be preferred over passing an AsyncFuture into a method since + * a CompletionToken provides both more flexibility in use (due to accepting + * an arbitrary callback) and more safety (since the RAII semantics guarantees + * that the callback is never silently dropped). + * + * The token may only be moved, not copied. + */ +class EXPCL_PANDA_PUTIL CompletionToken { +public: + constexpr CompletionToken() = default; + +#ifndef CPPPARSER + template + INLINE CompletionToken(Callable callback); +#endif + + void complete(bool success); + +protected: + Completable _callback; + + friend class CompletionCounter; +}; + +#include "completionToken.I" + +#endif diff --git a/panda/src/putil/p3putil_composite1.cxx b/panda/src/putil/p3putil_composite1.cxx index f78459eff7..c464d7708c 100644 --- a/panda/src/putil/p3putil_composite1.cxx +++ b/panda/src/putil/p3putil_composite1.cxx @@ -17,6 +17,7 @@ #include "callbackObject.cxx" #include "clockObject.cxx" #include "colorSpace.cxx" +#include "completionCounter.cxx" #include "config_putil.cxx" #include "configurable.cxx" #include "copyOnWriteObject.cxx" From 2a47826101b4ad798564a38bc3b38109994e4444 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 24 Jan 2025 22:39:31 +0100 Subject: [PATCH 032/291] glgsg: Big refactoring of texture uploading, add async upload support --- panda/src/display/graphicsStateGuardian.cxx | 17 + panda/src/display/graphicsStateGuardian.h | 1 + panda/src/gles2gsg/gles2gsg.h | 4 + .../glstuff/glGraphicsStateGuardian_src.cxx | 1619 +++++++++++------ .../src/glstuff/glGraphicsStateGuardian_src.h | 51 +- panda/src/glstuff/glTextureContext_src.I | 40 + panda/src/glstuff/glTextureContext_src.cxx | 60 + panda/src/glstuff/glTextureContext_src.h | 20 + panda/src/glstuff/glmisc_src.cxx | 5 + panda/src/glstuff/glmisc_src.h | 1 + panda/src/gobj/preparedGraphicsObjects.cxx | 21 +- panda/src/gobj/texture.I | 8 + panda/src/gobj/texture.cxx | 40 +- panda/src/gobj/texture.h | 6 + panda/src/gsgbase/graphicsStateGuardianBase.h | 2 + panda/src/putil/completable.I | 14 + panda/src/putil/completable.h | 3 + 17 files changed, 1373 insertions(+), 539 deletions(-) diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 9be45d34ef..80c02f88bb 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -572,6 +572,23 @@ update_texture(TextureContext *, bool) { return true; } +/** + * Ensures that the current Texture data is refreshed onto the GSG. This + * means updating the texture properties and/or re-uploading the texture + * image, if necessary. This should only be called within the draw thread. + * + * If force is true, this function will not return until the texture has been + * fully uploaded. If force is false, the function may choose to upload a + * simple version of the texture instead, if the texture is not fully resident + * (and if get_incomplete_render() is true). + */ +bool GraphicsStateGuardian:: +update_texture(TextureContext *tc, bool force, CompletionToken token) { + bool result = update_texture(tc, force); + token.complete(result); + return result; +} + /** * Frees the resources previously allocated via a call to prepare_texture(), * including deleting the TextureContext itself, if it is non-NULL. diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index 3ed65b8e31..039572a7af 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -292,6 +292,7 @@ PUBLISHED: public: virtual TextureContext *prepare_texture(Texture *tex); virtual bool update_texture(TextureContext *tc, bool force); + virtual bool update_texture(TextureContext *tc, bool force, CompletionToken token); virtual void release_texture(TextureContext *tc); virtual void release_textures(const pvector &contexts); virtual bool extract_texture_data(Texture *tex); diff --git a/panda/src/gles2gsg/gles2gsg.h b/panda/src/gles2gsg/gles2gsg.h index c43beded29..feb30b41a0 100644 --- a/panda/src/gles2gsg/gles2gsg.h +++ b/panda/src/gles2gsg/gles2gsg.h @@ -148,6 +148,10 @@ typedef char GLchar; #define GL_TRANSFORM_FEEDBACK_BARRIER_BIT 0x800 #define GL_ATOMIC_COUNTER_BARRIER_BIT 0x1000 #define GL_SHADER_STORAGE_BARRIER_BIT 0x2000 +#define GL_MAP_INVALIDATE_RANGE_BIT 0x0004 +#define GL_MAP_INVALIDATE_BUFFER_BIT 0x0008 +#define GL_MAP_FLUSH_EXPLICIT_BIT 0x0010 +#define GL_MAP_UNSYNCHRONIZED_BIT 0x0020 #define GL_HALF_FLOAT 0x140B #define GL_COLOR 0x1800 #define GL_DEPTH 0x1801 diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 139ec128a6..8317a326e9 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -68,6 +68,7 @@ #include "shaderGenerator.h" #include "samplerState.h" #include "displayInformation.h" +#include "completionCounter.h" #if defined(HAVE_CG) && !defined(OPENGLES) #include @@ -97,6 +98,10 @@ PStatCollector CLP(GraphicsStateGuardian)::_check_residency_pcollector("*:PStats PStatCollector CLP(GraphicsStateGuardian)::_wait_fence_pcollector("Wait:Fence"); PStatCollector CLP(GraphicsStateGuardian)::_copy_texture_finish_pcollector("Draw:Copy texture:Finish"); +static PStatCollector _create_texture_storage_pcollector("Draw:Transfer data:Texture:Create Storage"); +static PStatCollector _create_map_pbo_pcollector("Draw:Transfer data:Texture:Create/Map PBO"); +static PStatCollector _load_texture_copy_pcollector("Draw:Transfer data:Texture:Copy/Convert"); + #if defined(HAVE_CG) && !defined(OPENGLES) AtomicAdjust::Integer CLP(GraphicsStateGuardian)::_num_gsgs_with_cg_contexts = 0; small_vector CLP(GraphicsStateGuardian)::_destroyed_cg_contexts; @@ -326,6 +331,23 @@ uchar_l_to_rgb(unsigned char *dest, const unsigned char *source, } } +/** + * Recopies the given array of pixels, converting from luminance to RGBA + * arrangement. + */ +static void +uchar_l_to_rgba(unsigned char *dest, const unsigned char *source, + int num_pixels) { + for (int i = 0; i < num_pixels; i++) { + dest[0] = source[0]; + dest[1] = source[0]; + dest[2] = source[0]; + dest[3] = 1; + dest += 4; + source += 1; + } +} + /** * Recopies the given array of pixels, converting from BGRA to RGBA * arrangement. @@ -425,6 +447,187 @@ ushort_la_to_rgba(unsigned short *dest, const unsigned short *source, } } +/** + * Determines the number of components of the given external format. + */ +static int +get_external_format_components(GLint external_format) { + switch (external_format) { +#ifndef OPENGLES_1 + case GL_RED: + case GL_RED_INTEGER: + case GL_GREEN: + case GL_BLUE: +#endif + case GL_ALPHA: + case GL_LUMINANCE: +#ifndef OPENGLES + case GL_GREEN_INTEGER: + case GL_BLUE_INTEGER: + case GL_ALPHA_INTEGER: +#endif + return 1; + +#ifndef OPENGLES_1 + case GL_RG: + case GL_RG_INTEGER: +#endif + case GL_LUMINANCE_ALPHA: + return 2; + + case GL_RGB: +#ifndef OPENGLES_1 + case GL_RGB_INTEGER: +#endif +#ifndef OPENGLES + case GL_BGR: + case GL_BGR_INTEGER: +#endif + return 3; + + case GL_RGBA: +#ifndef OPENGLES_1 + case GL_RGBA_INTEGER: +#endif + case GL_BGRA: +#ifndef OPENGLES + case GL_BGRA_INTEGER: +#endif + return 4; + + default: + GLCAT.error() + << "Unknown external format 0x" << std::hex << external_format + << std::dec << "\n"; + return 4; + } +} + +/** + * Copies the image with optional conversion. + */ +static void +copy_image(unsigned char *new_image, const unsigned char *orig_image, + size_t orig_image_size, GLint external_format, int num_components, + int component_width) { + switch (external_format) { +#ifndef OPENGLES_1 + case GL_RED: + case GL_RED_INTEGER: + case GL_GREEN: + case GL_BLUE: +#endif + case GL_ALPHA: + case GL_LUMINANCE: +#ifndef OPENGLES + case GL_GREEN_INTEGER: + case GL_BLUE_INTEGER: + case GL_ALPHA_INTEGER: +#endif + if (num_components == 1) { + memcpy(new_image, orig_image, orig_image_size); + return; + } + break; + +#ifndef OPENGLES_1 + case GL_RG: + case GL_RG_INTEGER: +#endif + case GL_LUMINANCE_ALPHA: + if (num_components == 2) { + memcpy(new_image, orig_image, orig_image_size); + return; + } + break; + +#ifndef OPENGLES_1 +#ifndef OPENGLES + case GL_BGR: +#endif + case GL_RGB_INTEGER: + if (num_components == 3) { + memcpy(new_image, orig_image, orig_image_size); + return; + } + if (num_components == 1 && component_width == 1) { + uchar_l_to_rgb(new_image, orig_image, orig_image_size); + return; + } + break; +#endif + + case GL_RGB: +#ifndef OPENGLES + case GL_BGR_INTEGER: +#endif + // Need to swap order. + if (num_components == 1 && component_width == 1) { + uchar_l_to_rgb(new_image, orig_image, orig_image_size); + return; + } + if (num_components == 3 && component_width == 1) { + uchar_bgr_to_rgb(new_image, orig_image, orig_image_size / 3); + return; + } + if (num_components == 3 && component_width == 2) { + ushort_bgr_to_rgb((unsigned short *)new_image, + (const unsigned short *)orig_image, + orig_image_size / 6); + return; + } + break; + + case GL_BGRA: +#ifndef OPENGLES_1 + case GL_RGBA_INTEGER: +#endif + if (num_components == 4) { + memcpy(new_image, orig_image, orig_image_size); + return; + } + if (num_components == 1 && component_width == 1) { + uchar_l_to_rgba(new_image, orig_image, orig_image_size); + return; + } + if (num_components == 2 && component_width == 1) { + uchar_la_to_rgba(new_image, orig_image, orig_image_size / 2); + return; + } + break; + + case GL_RGBA: +#ifndef OPENGLES + case GL_BGRA_INTEGER: +#endif + // Need to swap order. + if (num_components == 1 && component_width == 1) { + uchar_l_to_rgba(new_image, orig_image, orig_image_size); + return; + } + if (num_components == 2 && component_width == 1) { + uchar_la_to_rgba(new_image, orig_image, orig_image_size / 2); + return; + } + if (num_components == 4 && component_width == 1) { + uchar_bgra_to_rgba(new_image, orig_image, orig_image_size / 4); + return; + } + if (num_components == 4 && component_width == 2) { + ushort_bgra_to_rgba((unsigned short *)new_image, + (const unsigned short *)orig_image, + orig_image_size / 8); + return; + } + break; + + default: + break; + } + + nassert_raise("Failed to convert image."); +} + /** * Reverses the order of the components within the image, to convert (for * instance) GL_BGR to GL_RGB. Returns the byte pointer representing the @@ -440,72 +643,15 @@ static const unsigned char * fix_component_ordering(PTA_uchar &new_image, const unsigned char *orig_image, size_t orig_image_size, GLenum external_format, Texture *tex) { - const unsigned char *result = orig_image; - - switch (external_format) { - case GL_RGB: - if (tex->get_num_components() == 1) { - new_image = PTA_uchar::empty_array(orig_image_size * 3); - uchar_l_to_rgb(new_image, orig_image, orig_image_size); - result = new_image; - break; - } - switch (tex->get_component_type()) { - case Texture::T_unsigned_byte: - case Texture::T_byte: - new_image = PTA_uchar::empty_array(orig_image_size); - uchar_bgr_to_rgb(new_image, orig_image, orig_image_size / 3); - result = new_image; - break; - - case Texture::T_unsigned_short: - case Texture::T_short: - new_image = PTA_uchar::empty_array(orig_image_size); - ushort_bgr_to_rgb((unsigned short *)new_image.p(), - (const unsigned short *)orig_image, - orig_image_size / 6); - result = new_image; - break; - - default: - break; - } - break; - - case GL_RGBA: - if (tex->get_num_components() == 2) { - new_image = PTA_uchar::empty_array(orig_image_size * 2); - uchar_la_to_rgba(new_image, orig_image, orig_image_size / 2); - result = new_image; - break; - } - switch (tex->get_component_type()) { - case Texture::T_unsigned_byte: - case Texture::T_byte: - new_image = PTA_uchar::empty_array(orig_image_size); - uchar_bgra_to_rgba(new_image, orig_image, orig_image_size / 4); - result = new_image; - break; - - case Texture::T_unsigned_short: - case Texture::T_short: - new_image = PTA_uchar::empty_array(orig_image_size); - ushort_bgra_to_rgba((unsigned short *)new_image.p(), - (const unsigned short *)orig_image, - orig_image_size / 8); - result = new_image; - break; - - default: - break; - } - break; - - default: - break; + if (external_format == GL_RGB || external_format == GL_RGBA) { + int num_components = tex->get_num_components(); + int component_width = tex->get_component_width(); + size_t new_image_size = (orig_image_size / num_components) * ((external_format == GL_RGBA) ? 4 : 3); + new_image = PTA_uchar::empty_array(new_image_size); + copy_image(&new_image[0], orig_image, orig_image_size, external_format, num_components, component_width); + return new_image; } - - return result; + return orig_image; } // #--- Zhao Nov2011 @@ -524,6 +670,7 @@ int CLP(GraphicsStateGuardian)::get_driver_shader_version_minor() { return _gl_s CLP(GraphicsStateGuardian):: CLP(GraphicsStateGuardian)(GraphicsEngine *engine, GraphicsPipe *pipe) : GraphicsStateGuardian(gl_coordinate_system, engine, pipe), + _job_queue_cvar(_job_queue_mutex), _renderbuffer_residency(get_prepared_objects()->get_name(), "renderbuffer"), _active_ppbuffer_memory_pcollector("Graphics memory:" + get_prepared_objects()->get_name() + ":Active:ppbuffer"), _inactive_ppbuffer_memory_pcollector("Graphics memory:" + get_prepared_objects()->get_name() + ":Inactive:ppbuffer") @@ -567,6 +714,13 @@ CLP(GraphicsStateGuardian)(GraphicsEngine *engine, GraphicsPipe *pipe) : _cg_context = 0; #endif +#ifdef HAVE_THREADS + //XXX having a fixed number of threads is not a great idea. We ought to have + // a common thread pool that is sized based on the available number of CPUs. + AsyncTaskManager *task_mgr = AsyncTaskManager::get_global_ptr(); + _async_chain = task_mgr->make_task_chain("texture_copy", 2, TP_normal); +#endif + #ifdef DO_PSTATS if (gl_finish) { GLCAT.warning() @@ -585,6 +739,15 @@ CLP(GraphicsStateGuardian):: << "GLGraphicsStateGuardian " << this << " destructing\n"; } +#ifdef HAVE_THREADS + // Make sure there are no more async tasks that could reference this GSG. + _async_chain->wait_for_tasks(); +#endif + { + MutexHolder holder(_job_queue_mutex); + _job_queue.clear(); + } + close_gsg(); } @@ -1731,6 +1894,24 @@ reset() { } #endif +#ifndef OPENGLES_1 + _glCopyBufferSubData = nullptr; + if (_supports_buffers) { + if (is_at_least_gl_version(3, 1) || + is_at_least_gles_version(3, 0) || + has_extension("GL_ARB_copy_buffer")) { + _glCopyBufferSubData = (PFNGLCOPYBUFFERSUBDATAPROC) + get_extension_func("glCopyBufferSubData"); + } +#ifdef OPENGLES_2 + else if (has_extension("GL_NV_copy_buffer")) { + _glCopyBufferSubData = (PFNGLCOPYBUFFERSUBDATAPROC) + get_extension_func("glCopyBufferSubDataNV"); + } +#endif + } +#endif + #ifdef OPENGLES if (is_at_least_gles_version(3, 0)) { _glMapBufferRange = (PFNGLMAPBUFFERRANGEEXTPROC) @@ -1757,7 +1938,8 @@ reset() { _glMapBufferRange = nullptr; } - if (is_at_least_gl_version(4, 4) || has_extension("GL_ARB_buffer_storage")) { + if (_glMapBufferRange != nullptr && + (is_at_least_gl_version(4, 4) || has_extension("GL_ARB_buffer_storage"))) { _glBufferStorage = (PFNGLBUFFERSTORAGEPROC) get_extension_func("glBufferStorage"); @@ -2593,7 +2775,10 @@ reset() { #endif #ifndef OPENGLES - if (is_at_least_gl_version(4, 5) || has_extension("GL_ARB_direct_state_access")) { + _glMapNamedBufferRange = nullptr; + + if (gl_support_dsa && + (is_at_least_gl_version(4, 5) || has_extension("GL_ARB_direct_state_access"))) { _glCreateTextures = (PFNGLCREATETEXTURESPROC) get_extension_func("glCreateTextures"); _glTextureStorage2D = (PFNGLTEXTURESTORAGE2DPROC) @@ -2607,12 +2792,29 @@ reset() { _glBindTextureUnit = (PFNGLBINDTEXTUREUNITPROC) get_extension_func("glBindTextureUnit"); + if (_glMapBufferRange != nullptr) { + _glMapNamedBufferRange = (PFNGLMAPNAMEDBUFFERRANGEPROC) + get_extension_func("glMapNamedBufferRange"); + } + _supports_dsa = true; } else { _supports_dsa = false; } #endif +#ifndef OPENGLES_1 +#ifdef OPENGLES + if (is_at_least_gles_version(3, 0) || has_extension("GL_NV_pixel_buffer_object")) { +#else + if (is_at_least_gl_version(2, 1) || has_extension("GL_ARB_pixel_buffer_object")) { +#endif + _supports_pixel_buffers = true; + } else { + _supports_pixel_buffers = false; + } +#endif + #ifndef OPENGLES_1 // Do we support empty framebuffer objects? #ifdef OPENGLES @@ -4279,6 +4481,8 @@ prepare_lens() { */ bool CLP(GraphicsStateGuardian):: begin_frame(Thread *current_thread) { + process_pending_jobs(false); + if (!GraphicsStateGuardian::begin_frame(current_thread)) { return false; } @@ -6369,23 +6573,16 @@ prepare_texture(Texture *tex) { * (and if get_incomplete_render() is true). */ bool CLP(GraphicsStateGuardian):: -update_texture(TextureContext *tc, bool force) { +update_texture(TextureContext *tc, bool force, CompletionToken token) { CLP(TextureContext) *gtc; DCAST_INTO_R(gtc, tc, false); - Texture *tex = tc->get_texture(); - GLenum target = get_texture_target(tex->get_texture_type()); - if (gtc->_target != target) { - // The target has changed. That means we have to re-bind a new texture - // object. - gtc->reset_data(target, tex->get_num_views()); - } - if (gtc->was_image_modified() || !gtc->_has_storage) { PStatGPUTimer timer(this, _texture_update_pcollector); // If the texture image was modified, reload the texture. - bool okflag = upload_texture(gtc, force, tex->uses_mipmaps()); + Texture *tex = tc->get_texture(); + bool okflag = upload_texture(gtc, force, tex->uses_mipmaps(), std::move(token)); if (!okflag) { GLCAT.error() << "Could not load " << *tex << "\n"; @@ -6401,6 +6598,7 @@ update_texture(TextureContext *tc, bool force) { } else if (gtc->was_properties_modified()) { PStatGPUTimer timer(this, _texture_update_pcollector); + Texture *tex = tc->get_texture(); // If only the properties have been modified, we don't necessarily need to // reload the texture. @@ -6416,7 +6614,7 @@ update_texture(TextureContext *tc, bool force) { if (needs_reload) { gtc->mark_needs_reload(); - bool okflag = upload_texture(gtc, force, tex->uses_mipmaps()); + bool okflag = upload_texture(gtc, force, tex->uses_mipmaps(), std::move(token)); if (!okflag) { GLCAT.error() << "Could not load " << *tex << "\n"; @@ -6426,8 +6624,21 @@ update_texture(TextureContext *tc, bool force) { else { // The texture didn't need reloading, but mark it fully updated now. gtc->mark_loaded(); + + if (force) { + // This update is still underway. + gtc->wait_pending_uploads(); + } + token.complete(true); } } + else { + if (force) { + // This update is still underway. + gtc->wait_pending_uploads(); + } + token.complete(true); + } gtc->enqueue_lru(&_prepared_objects->_graphics_memory_lru); @@ -6444,6 +6655,10 @@ void CLP(GraphicsStateGuardian):: release_texture(TextureContext *tc) { CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc); + gtc->cancel_pending_uploads(); + gtc->wait_pending_uploads(); + gtc->delete_unused_pbos(); + gtc->set_num_views(0); delete gtc; } @@ -7793,6 +8008,8 @@ framebuffer_copy_to_texture(Texture *tex, int view, int z, nassertr(tc != nullptr, false); CLP(TextureContext) *gtc = DCAST(CLP(TextureContext), tc); + gtc->cancel_pending_uploads(); + GLenum target = get_texture_target(tex->get_texture_type()); if (gtc->_target != target) { gtc->reset_data(target, view + 1); @@ -8300,13 +8517,6 @@ finish_async_framebuffer_ram_copies(bool force) { return; } - //XXX having a fixed number of threads is not a great idea. We ought to have - // a common thread pool that is sized based on the available number of CPUs. -#ifdef HAVE_THREADS - AsyncTaskManager *task_mgr = AsyncTaskManager::get_global_ptr(); - static AsyncTaskChain *chain = task_mgr->make_task_chain("texture_download", 2, TP_low); -#endif - PStatTimer timer(_copy_texture_finish_pcollector); if (force) { @@ -8369,7 +8579,7 @@ finish_async_framebuffer_ram_copies(bool force) { // earlier frames will be processed before subsequent frames, but we don't // make it unique for every frame, which would kill concurrency. int frame_number = request->get_frame_number(); - chain->add(std::move(func), "screenshot", frame_number >> 3, -(frame_number & ((1 << 3) - 1))); + _async_chain->add(std::move(func), "screenshot", frame_number >> 3, -(frame_number & ((1 << 3) - 1))); #else func(nullptr); #endif @@ -13858,12 +14068,15 @@ apply_sampler(GLuint unit, const SamplerState &sampler, CLP(TextureContext) *gtc * image. */ bool CLP(GraphicsStateGuardian):: -upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) { +upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps, + CompletionToken token) { PStatGPUTimer timer(this, _load_texture_pcollector); Texture *tex = gtc->get_texture(); - if (_effective_incomplete_render && !force) { + //FIXME: upload simple texture for async uploaded thing + bool async_upload = true; + if (_effective_incomplete_render && !force && !async_upload) { bool has_image = _supports_compressed_texture ? tex->has_ram_image() : tex->has_uncompressed_ram_image(); if (!has_image && tex->might_have_ram_image() && tex->has_simple_ram_image() && @@ -14147,6 +14360,8 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) { int num_views = tex->get_num_views(); if (needs_reload) { + gtc->cancel_pending_uploads(); + if (gtc->_immutable) { GLCAT.info() << "Attempt to modify texture with immutable storage, recreating texture.\n"; @@ -14178,46 +14393,73 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) { nassertr(gtc->_buffers != nullptr, false); } - bool extract_success = false; - if (tex->get_post_load_store_cache()) { - extract_success = true; + bool compressed = image_compression != Texture::CM_off; + if (compressed && !_supports_compressed_texture) { + return false; } + // Does this texture require asynchronous uploads? +#if defined(HAVE_THREADS) && !defined(OPENGLES_1) + int async_buffers = _supports_pixel_buffers ? tex->get_num_async_transfer_buffers() : 0; + if (async_buffers != 0) { + // Prefer immutable storage, if supported. + if (needs_reload && _supports_tex_storage) { + gtc->_immutable = true; + } + } + else if (async_buffers == 0 && gtc->_num_pbos > 0) { + gtc->delete_unused_pbos(); + } +#else + int async_buffers = 0; +#endif + + // Keep track of which views are uploaded. + CompletionCounter counter; bool success = true; + for (int view = 0; view < num_views; ++view) { - if (upload_texture_image(gtc, view, needs_reload || view >= old_num_views, - mipmap_bias, num_levels, - internal_format, external_format, - component_type, image_compression)) { + if (upload_texture_view(gtc, view, needs_reload || view >= old_num_views, + mipmap_bias, num_levels, + internal_format, external_format, + component_type, compressed, async_buffers, + counter.make_token())) { + // We always create storage right away even if we do the upload of the + // actual data asynchronously. gtc->_has_storage = true; gtc->_internal_format = internal_format; gtc->_width = width; gtc->_height = height; gtc->_depth = depth; gtc->_num_levels = num_levels; - - if (extract_success) { - // The next call assumes the texture is still bound. - if (!do_extract_texture_data(gtc, view)) { - extract_success = false; - } - } } else { success = false; } } - report_my_gl_errors(); + if (!success) { + report_my_gl_errors(); + return false; + } + + gtc->_uploads_pending++; + + std::move(counter).then([=, token = std::move(token)] (bool success) mutable { + --gtc->_uploads_pending; + if (!success) { + token.complete(false); + return; + } - if (success) { if (needs_reload) { gtc->update_data_size_bytes(get_texture_memory_size(gtc)); } - nassertr(gtc->_has_storage, false); + nassertv(gtc->_has_storage); - if (extract_success) { + Texture *tex = gtc->get_texture(); + if (tex->get_post_load_store_cache()) { tex->set_post_load_store_cache(false); // OK, get the RAM image, and save it in a BamCache record. if (tex->has_ram_image()) { @@ -14231,14 +14473,20 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) { } GraphicsEngine *engine = get_engine(); - nassertr(engine != nullptr, false); + nassertv(engine != nullptr); engine->texture_uploaded(tex); - gtc->mark_loaded(); - return true; - } + token.complete(true); + }); - return false; + // Update the modified counters now, even if we've only spawned an async + // upload, because we've already set things in motion to update the texture + // to this version. Otherwise, future calls to update_texture will continue + // to try to update the image over and over again. + gtc->mark_loaded(); + + report_my_gl_errors(); + return true; } /** @@ -14246,17 +14494,13 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps) { * texture memory. */ bool CLP(GraphicsStateGuardian):: -upload_texture_image(CLP(TextureContext) *gtc, int view, bool needs_reload, - int mipmap_bias, int num_levels, GLint internal_format, - GLint external_format, GLenum component_type, - Texture::CompressionMode image_compression) { +upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload, + int mipmap_bias, int num_levels, GLint internal_format, + GLint external_format, GLenum component_type, + bool compressed, int async_buffers, CompletionToken token) { // Make sure the error stack is cleared out before we begin. clear_my_gl_errors(); - if (image_compression != Texture::CM_off && !_supports_compressed_texture) { - return false; - } - GLenum target = gtc->_target; if (target == GL_NONE) { // Unsupported target (e.g. 3-d texturing on GL 1.1). @@ -14293,9 +14537,14 @@ upload_texture_image(CLP(TextureContext) *gtc, int view, bool needs_reload, int width = tex->get_expected_mipmap_x_size(mipmap_bias); int height = tex->get_expected_mipmap_y_size(mipmap_bias); int depth = tex->get_expected_mipmap_z_size(mipmap_bias); + int num_components = tex->get_num_components(); + int expected_num_components = compressed ? num_components : get_external_format_components(external_format); + int component_width = tex->get_component_width(); + GLenum usage = GL_STATIC_DRAW; #ifndef OPENGLES_1 if (target == GL_TEXTURE_BUFFER) { + usage = get_usage(tex->get_usage_hint()); GLuint buffer = gtc->get_view_buffer(view); nassertr(buffer != 0, false); _glBindBuffer(GL_TEXTURE_BUFFER, buffer); @@ -14363,6 +14612,7 @@ upload_texture_image(CLP(TextureContext) *gtc, int view, bool needs_reload, // but we are not allowed to change the texture size or number of mipmap // levels after this point. if (gtc->_immutable) { + PStatTimer timer(_create_texture_storage_pcollector); if (GLCAT.is_debug()) { GLCAT.debug() << "allocating storage for texture " << tex->get_name() << ", " @@ -14397,15 +14647,84 @@ upload_texture_image(CLP(TextureContext) *gtc, int view, bool needs_reload, // How many mipmap levels do we have available to upload? int num_ram_mipmap_levels = 0; + GLuint pbo = 0; + size_t pbo_size = 0; + void *mapped_ptr = nullptr; if (!image.is_null()) { num_ram_mipmap_levels = std::min(num_levels, tex->get_num_ram_mipmap_images() - mipmap_bias); + + // Create a PBO that can hold all the mipmap levels. +#if defined(HAVE_THREADS) && !defined(OPENGLES_1) + if (async_buffers != 0) { + pbo_size = 0; + for (int n = mipmap_bias; n < num_ram_mipmap_levels + mipmap_bias; ++n) { + size_t view_size = tex->get_ram_mipmap_view_size(n); + if (!compressed) { + view_size = expected_num_components * (view_size / num_components); + } + pbo_size += view_size; + } + + bool create_storage = false; + if (pbo_size != gtc->_pbo_size) { + // No PBOs yet, or they aren't of the right size. + if (_supports_buffer_storage) { + // If using buffer storage, need to deallocate all of them. + gtc->delete_unused_pbos(); + } + gtc->_pbo_size = pbo_size; + create_storage = true; + } + else if (async_buffers > 0) { + // Wait for a PBO to become available if we're at our limit. + gtc->wait_for_unused_pbo(async_buffers); + } + + PStatTimer timer(_create_map_pbo_pcollector); + if (gtc->_unused_pbos.empty()) { + _glGenBuffers(1, &pbo); + gtc->_num_pbos++; + create_storage = true; + } else { + // Map an existing PBO. + pbo = gtc->_unused_pbos.back(); + gtc->_unused_pbos.pop_back(); + } + + mapped_ptr = map_write_discard_buffer(GL_PIXEL_UNPACK_BUFFER, pbo, + pbo_size, create_storage); + if (mapped_ptr == nullptr) { + report_my_gl_errors(); + GLCAT.warning() + << "Failed to map pixel unpack buffer.\n"; + gtc->_unused_pbos.push_back(pbo); + } + } +#endif } - if (!needs_reload) { - // Try to subload the image over the existing GL Texture object, possibly - // saving on texture memory fragmentation. + if (needs_reload && num_ram_mipmap_levels == 0 && + external_format == GL_DEPTH_STENCIL && get_supports_depth_stencil()) { +#ifdef OPENGLES + component_type = GL_UNSIGNED_INT_24_8_OES; +#else + component_type = GL_UNSIGNED_INT_24_8_EXT; +#endif + } - if (GLCAT.is_debug()) { + int upload_count = ++gtc->_uploads_started; + + if (GLCAT.is_debug()) { + if (needs_reload) { + // Load the image up from scratch, creating a new GL Texture object. + GLCAT.debug() + << "loading new texture object for " << tex->get_name() << " view " + << view << ", " << width << " x " << height << " x " << depth + << ", mipmaps " << num_ram_mipmap_levels << " / " << num_levels; + } + else { + // Try to subload the image over the existing GL Texture object, possibly + // saving on texture memory fragmentation. SparseArray pages = gtc->get_view_modified_pages(view, 0); if (num_ram_mipmap_levels == 0) { if (tex->has_clear_color()) { @@ -14413,433 +14732,272 @@ upload_texture_image(CLP(TextureContext) *gtc, int view, bool needs_reload, << "clearing texture " << tex->get_name() << " view " << view << ", " << width << " x " << height << " x " << depth << ", pages " << pages << ", mipmaps " << num_levels << ", clear_color = " - << tex->get_clear_color() << "\n"; + << tex->get_clear_color(); } else { GLCAT.debug() << "not loading NULL image for texture " << tex->get_name() << " view " << view << ", " << width << " x " << height << " x " << depth - << ", pages " << pages << ", mipmaps = " << num_levels << "\n"; + << ", pages " << pages << ", mipmaps = " << num_levels; } } else { GLCAT.debug() << "updating image data of texture " << tex->get_name() << " view " << view << ", " << width << " x " << height << " x " << depth << ", pages " << pages << ", mipmaps " << num_ram_mipmap_levels - << " / " << num_levels << "\n"; + << " / " << num_levels; } } - - for (int n = mipmap_bias; n < num_levels + mipmap_bias; ++n) { - SparseArray pages = gtc->get_view_modified_pages(view, n); - - // we grab the mipmap pointer first, if it is NULL we grab the normal - // mipmap image pointer which is a PTA_uchar - const unsigned char *image_ptr = (unsigned char*)tex->get_ram_mipmap_pointer(n); - CPTA_uchar ptimage; - if (image_ptr == nullptr) { - ptimage = tex->get_ram_mipmap_image(n); - if (ptimage.is_null()) { - if (n - mipmap_bias < num_ram_mipmap_levels) { - // We were told we'd have this many RAM mipmap images, but we - // don't. Raise a warning. - GLCAT.warning() - << "No mipmap level " << n << " defined for " << tex->get_name() - << "\n"; - break; - } - - if (tex->has_clear_color()) { - // The texture has a clear color, so we should fill this mipmap - // level to a solid color. -#ifndef OPENGLES - if (target != GL_TEXTURE_BUFFER) { - if (_supports_clear_texture) { - // We can do that with the convenient glClearTexImage - // function. - vector_uchar clear_data = tex->get_clear_data(); - - if (pages.has_all_of(0, depth)) { - _glClearTexImage(index, n - mipmap_bias, external_format, - component_type, (void *)&clear_data[0]); - } - else for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) { - int begin = pages.get_subrange_begin(sri); - int num_pages = pages.get_subrange_end(sri) - begin; - _glClearTexSubImage(index, n - mipmap_bias, 0, 0, begin, - width, height, num_pages, external_format, - component_type, (void *)&clear_data[0]); - } - continue; - } - } else { - if (_supports_clear_buffer) { - // For buffer textures we need to clear the underlying - // storage. - vector_uchar clear_data = tex->get_clear_data(); - - _glClearBufferData(GL_TEXTURE_BUFFER, internal_format, external_format, - component_type, (const void *)&clear_data[0]); - continue; - } - } -#endif // OPENGLES - // Ask the Texture class to create the mipmap level in RAM. It'll - // fill it in with the correct clear color, which we can then - // upload. - ptimage = tex->make_ram_mipmap_image(n); - - } else { - // No clear color and no more images. - break; - } - } - image_ptr = ptimage; - } - - PTA_uchar bgr_image; - size_t page_size = tex->get_ram_mipmap_page_size(n); - if (image_ptr != nullptr) { - const unsigned char *orig_image_ptr = image_ptr; - size_t view_size = tex->get_ram_mipmap_view_size(n); - image_ptr += view_size * view; - nassertr(image_ptr >= orig_image_ptr && image_ptr + view_size <= orig_image_ptr + tex->get_ram_mipmap_image_size(n), false); - - if (image_compression == Texture::CM_off) { - // If the GL doesn't claim to support BGR, we may have to reverse - // the component ordering of the image. - image_ptr = fix_component_ordering(bgr_image, image_ptr, view_size, - external_format, tex); - } - } - - int width = tex->get_expected_mipmap_x_size(n); - int height = tex->get_expected_mipmap_y_size(n); - -#ifdef DO_PSTATS - _data_transferred_pcollector.add_level(page_size * pages.get_num_on_bits()); -#endif - switch (target) { -#ifndef OPENGLES_1 - case GL_TEXTURE_3D: - if (_supports_3d_texture) { - for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) { - int begin = pages.get_subrange_begin(sri); - int num_pages = pages.get_subrange_end(sri) - begin; - const unsigned char *page_ptr = image_ptr + page_size * begin; - - if (image_compression == Texture::CM_off) { - _glTexSubImage3D(target, n - mipmap_bias, - 0, 0, begin, width, height, num_pages, - external_format, component_type, page_ptr); - } else { - _glCompressedTexSubImage3D(target, n - mipmap_bias, - 0, 0, begin, width, height, num_pages, - external_format, - page_size * num_pages, page_ptr); - } - } - } else { - report_my_gl_errors(); - return false; - } - break; -#endif // OPENGLES_1 - -#ifndef OPENGLES - case GL_TEXTURE_1D: - if (image_compression == Texture::CM_off) { - glTexSubImage1D(target, n - mipmap_bias, 0, width, - external_format, component_type, image_ptr); - } else { - _glCompressedTexSubImage1D(target, n - mipmap_bias, 0, width, - external_format, page_size, image_ptr); - } - break; -#endif // OPENGLES - -#ifndef OPENGLES_1 - case GL_TEXTURE_2D_ARRAY: - case GL_TEXTURE_CUBE_MAP_ARRAY: - if (_supports_2d_texture_array) { - for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) { - int begin = pages.get_subrange_begin(sri); - int num_pages = pages.get_subrange_end(sri) - begin; - const unsigned char *page_ptr = image_ptr + page_size * begin; - - if (image_compression == Texture::CM_off) { - _glTexSubImage3D(target, n - mipmap_bias, - 0, 0, begin, width, height, num_pages, - external_format, component_type, page_ptr); - } else { - _glCompressedTexSubImage3D(target, n - mipmap_bias, - 0, 0, begin, width, height, num_pages, - external_format, - page_size * num_pages, page_ptr); - } - } - } else { - report_my_gl_errors(); - return false; - } - break; -#endif // OPENGLES_1 - -#ifndef OPENGLES_1 - case GL_TEXTURE_BUFFER: - if (_supports_buffer_texture) { - _glBufferSubData(GL_TEXTURE_BUFFER, 0, page_size, image_ptr); - } else { - report_my_gl_errors(); - return false; - } - break; -#endif // OPENGLES - - case GL_TEXTURE_CUBE_MAP: - if (_supports_cube_map) { - // This is the only texture type that must be specified using separate - // per-page calls. - if (n == 0) { - height = tex->get_y_size() - tex->get_pad_y_size(); - } - for (int z = 0; z < 6; ++z) { - if (pages.get_bit(z)) { - GLenum page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z; - const unsigned char *page_ptr = image_ptr + page_size * z; - - if (image_compression == Texture::CM_off) { - glTexSubImage2D(page_target, n - mipmap_bias, 0, 0, width, height, - external_format, component_type, page_ptr); - } else { - _glCompressedTexSubImage2D(page_target, n - mipmap_bias, - 0, 0, width, height, - external_format, page_size, page_ptr); - } - } - } - } else { - report_my_gl_errors(); - return false; - } - break; - - default: - if (image_compression == Texture::CM_off) { - if (n == 0) { - // It's unfortunate that we can't adjust the width, too, but - // TexSubImage2D doesn't accept a row-stride parameter. - height = tex->get_y_size() - tex->get_pad_y_size(); - } - glTexSubImage2D(target, n - mipmap_bias, 0, 0, width, height, - external_format, component_type, image_ptr); - } else { - _glCompressedTexSubImage2D(target, n - mipmap_bias, 0, 0, width, height, - external_format, page_size, image_ptr); - } - break; - } - } - - // Did that fail? If it did, we'll immediately try again, this time - // loading the texture from scratch. - GLenum error_code = gl_get_error(); - if (error_code != GL_NO_ERROR) { - if (GLCAT.is_debug()) { - GLCAT.debug() - << "GL texture subload failed for " << tex->get_name() - << " : " << get_error_string(error_code) << "\n"; - } - needs_reload = true; + if (mapped_ptr != nullptr) { + GLCAT.debug(false) << " (async #" << upload_count << " via buffer " << pbo << ")\n"; + } else { + GLCAT.debug(false) << " (#" << upload_count << ")\n"; } } - if (needs_reload) { - // Load the image up from scratch, creating a new GL Texture object. - if (GLCAT.is_debug()) { - GLCAT.debug() - << "loading new texture object for " << tex->get_name() << " view " - << view << ", " << width << " x " << height << " x " << depth - << ", mipmaps " << num_ram_mipmap_levels << " / " << num_levels << "\n"; - } - - // If there is immutable storage, this is impossible to do, and we should - // not have gotten here at all. - nassertr(!gtc->_immutable, false); - - if (num_ram_mipmap_levels == 0) { - if (GLCAT.is_debug()) { - GLCAT.debug() - << " (initializing NULL image)\n"; - } - - if ((external_format == GL_DEPTH_STENCIL) && get_supports_depth_stencil()) { -#ifdef OPENGLES - component_type = GL_UNSIGNED_INT_24_8_OES; -#else - component_type = GL_UNSIGNED_INT_24_8_EXT; + // Keeps track of any async jobs we've spawned. +#if defined(HAVE_THREADS) && !defined(OPENGLES_1) + CompletionCounter counter; + struct AsyncLevel { + int width, height, depth; + size_t page_size; + uintptr_t pbo_offset; + SparseArray pages; + }; + AsyncLevel *async_levels = nullptr; + size_t num_async_levels = 0; + uintptr_t pbo_offset = 0u; #endif - } + bool success = true; + + for (int n = mipmap_bias; n < num_levels + mipmap_bias; ++n) { + SparseArray pages = gtc->get_view_modified_pages(view, n); + int level = n - mipmap_bias; + + int width = tex->get_expected_mipmap_x_size(n); + int height = tex->get_expected_mipmap_y_size(n); + + // we grab the mipmap pointer first, if it is NULL we grab the normal + // mipmap image pointer which is a PTA_uchar + const unsigned char *image_ptr = (unsigned char*)tex->get_ram_mipmap_pointer(n); + CPTA_uchar ptimage; + if (image_ptr == nullptr) { + ptimage = tex->get_ram_mipmap_image(n); + image_ptr = ptimage; } + if (image_ptr == nullptr) { + if (level < num_ram_mipmap_levels) { + // We were told we'd have this many RAM mipmap images, but we + // don't. Raise a warning. + GLCAT.warning() + << "No mipmap level " << n << " defined for " << tex->get_name() + << "\n"; - for (int n = mipmap_bias; n < num_levels + mipmap_bias; ++n) { - const unsigned char *image_ptr = (unsigned char*)tex->get_ram_mipmap_pointer(n); - CPTA_uchar ptimage; - if (image_ptr == nullptr) { - ptimage = tex->get_ram_mipmap_image(n); - if (ptimage.is_null()) { - if (n - mipmap_bias < num_ram_mipmap_levels) { - // We were told we'd have this many RAM mipmap images, but we - // don't. Raise a warning. - GLCAT.warning() - << "No mipmap level " << n << " defined for " << tex->get_name() - << "\n"; - if (_supports_texture_max_level) { - // Tell the GL we have no more mipmaps for it to use. - glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, n - mipmap_bias); - } - break; - } + if (needs_reload && _supports_texture_max_level) { + // Tell the GL we have no more mipmaps for it to use. + glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, level - 1); + } + break; + } - if (tex->has_clear_color()) { - // Ask the Texture class to create the mipmap level in RAM. It'll - // fill it in with the correct clear color, which we can then - // upload. - ptimage = tex->make_ram_mipmap_image(n); - } - else if (image_compression != Texture::CM_off) { - // We can't upload a NULL compressed texture. - if (_supports_texture_max_level) { - // Tell the GL we have no more mipmaps for it to use. - glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, n - mipmap_bias); + if (tex->has_clear_color()) { + // The texture has a clear color, so we should fill this mipmap + // level to a solid color. +#ifndef OPENGLES + if (target != GL_TEXTURE_BUFFER) { + if (_supports_clear_texture && !needs_reload) { + // We can do that with the convenient glClearTexImage + // function. + vector_uchar clear_data = tex->get_clear_data(); + + if (pages.has_all_of(0, depth)) { + _glClearTexImage(index, level, external_format, + component_type, (void *)&clear_data[0]); } - break; + else for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) { + int begin = pages.get_subrange_begin(sri); + int num_pages = pages.get_subrange_end(sri) - begin; + _glClearTexSubImage(index, level, 0, 0, begin, + width, height, num_pages, external_format, + component_type, (void *)&clear_data[0]); + } + continue; + } + } else { + if (_supports_clear_buffer && !needs_reload) { + // For buffer textures we need to clear the underlying + // storage. + vector_uchar clear_data = tex->get_clear_data(); + + _glClearBufferData(GL_TEXTURE_BUFFER, internal_format, external_format, + component_type, (const void *)&clear_data[0]); + continue; } } +#endif // OPENGLES + // Ask the Texture class to create the mipmap level in RAM. It'll + // fill it in with the correct clear color, which we can then + // upload. + ptimage = tex->make_ram_mipmap_image(n); image_ptr = ptimage; } - - PTA_uchar bgr_image; - size_t view_size = tex->get_ram_mipmap_view_size(n); - if (image_ptr != nullptr) { - const unsigned char *orig_image_ptr = image_ptr; - image_ptr += view_size * view; - nassertr(image_ptr >= orig_image_ptr && image_ptr + view_size <= orig_image_ptr + tex->get_ram_mipmap_image_size(n), false); - - if (image_compression == Texture::CM_off) { - // If the GL doesn't claim to support BGR, we may have to reverse - // the component ordering of the image. - image_ptr = fix_component_ordering(bgr_image, image_ptr, view_size, - external_format, tex); - } + else if (!needs_reload) { + // No clear color and no more images, and no storage to create. + break; } - - int width = tex->get_expected_mipmap_x_size(n); - int height = tex->get_expected_mipmap_y_size(n); -#ifndef OPENGLES_1 - int depth = tex->get_expected_mipmap_z_size(n); -#endif - -#ifdef DO_PSTATS - _data_transferred_pcollector.add_level(view_size); -#endif - switch (target) { -#ifndef OPENGLES // 1-d textures not supported by OpenGL ES. Fall through. - case GL_TEXTURE_1D: - if (image_compression == Texture::CM_off) { - glTexImage1D(target, n - mipmap_bias, internal_format, - width, 0, external_format, component_type, image_ptr); - } else { - _glCompressedTexImage1D(target, n - mipmap_bias, external_format, - width, 0, view_size, image_ptr); + else if (compressed) { + // We can't upload a NULL compressed texture. + if (_supports_texture_max_level) { + // Tell the GL we have no more mipmaps for it to use. + glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, level - 1); } break; -#endif // OPENGLES // OpenGL ES will fall through. - -#ifndef OPENGLES_1 - case GL_TEXTURE_3D: - if (_supports_3d_texture) { - if (image_compression == Texture::CM_off) { - _glTexImage3D(target, n - mipmap_bias, internal_format, - width, height, depth, 0, - external_format, component_type, image_ptr); - } else { - _glCompressedTexImage3D(target, n - mipmap_bias, external_format, - width, height, depth, 0, view_size, image_ptr); - } - } else { - report_my_gl_errors(); - return false; - } - break; -#endif // OPENGLES_1 - -#ifndef OPENGLES_1 - case GL_TEXTURE_2D_ARRAY: - case GL_TEXTURE_CUBE_MAP_ARRAY: - if (_supports_2d_texture_array) { - if (image_compression == Texture::CM_off) { - _glTexImage3D(target, n - mipmap_bias, internal_format, - width, height, depth, 0, - external_format, component_type, image_ptr); - } else { - _glCompressedTexImage3D(target, n - mipmap_bias, external_format, - width, height, depth, 0, view_size, image_ptr); - } - } else { - report_my_gl_errors(); - return false; - } - break; - - case GL_TEXTURE_BUFFER: - if (_supports_buffer_texture) { - _glBufferData(GL_TEXTURE_BUFFER, view_size, image_ptr, - get_usage(tex->get_usage_hint())); - } else { - report_my_gl_errors(); - return false; - } - break; -#endif // OPENGLES_1 - - case GL_TEXTURE_CUBE_MAP: - if (_supports_cube_map) { - // This is the only texture type that must be specified using separate - // per-page calls. - size_t page_size = tex->get_ram_mipmap_page_size(n); - for (int z = 0; z < 6; ++z) { - GLenum page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z; - const unsigned char *page_ptr = - (image_ptr != nullptr) ? image_ptr + page_size * z : nullptr; - - if (image_compression == Texture::CM_off) { - glTexImage2D(page_target, n - mipmap_bias, internal_format, - width, height, 0, - external_format, component_type, page_ptr); - } else { - _glCompressedTexImage2D(page_target, n - mipmap_bias, external_format, - width, height, 0, page_size, page_ptr); - } - } - } else { - report_my_gl_errors(); - return false; - } - break; - - default: - if (image_compression == Texture::CM_off) { - glTexImage2D(target, n - mipmap_bias, internal_format, - width, height, 0, - external_format, component_type, image_ptr); - } else { - _glCompressedTexImage2D(target, n - mipmap_bias, external_format, - width, height, 0, view_size, image_ptr); - } } } - // Report the error message explicitly if the GL texture creation failed. + // Select the correct view. + size_t orig_view_size = 0; + size_t orig_page_size = 0; + size_t page_size = 0; + if (image_ptr != nullptr) { + orig_view_size = tex->get_ram_mipmap_view_size(n); + if (view > 0) { + const unsigned char *orig_image_ptr = image_ptr; + image_ptr += orig_view_size * view; + nassertd(image_ptr >= orig_image_ptr && image_ptr + orig_view_size <= orig_image_ptr + tex->get_ram_mipmap_image_size(n)) { + success = false; + break; + } + } + + orig_page_size = tex->get_ram_mipmap_page_size(n); + page_size = orig_page_size; + if (!compressed) { + // May need to convert. + page_size = get_external_format_components(external_format) * (page_size / num_components); + } + } +#ifndef OPENGLES_1 + else if (target == GL_TEXTURE_BUFFER) { + // page_size for buffer texture indicates the size even for a null image. + page_size = tex->get_expected_ram_mipmap_view_size(n); + } +#endif + + // Don't need to update the padded area at the bottom. + int sub_height = height; + if (n == 0 && (target == GL_TEXTURE_2D || target == GL_TEXTURE_CUBE_MAP)) { + sub_height -= tex->get_pad_y_size(); + } + +#if defined(HAVE_THREADS) && !defined(OPENGLES_1) + if (mapped_ptr != nullptr) { + // Let's make sure we have texture storage (normally this is handled by + // the glTexStorage2D calls above, if immutable texture storage is + // supported and enabled), it makes other things easier down the line. + if (needs_reload) { + PStatTimer timer(_create_texture_storage_pcollector); + if (!upload_texture_level(true, compressed, target, level, + width, height, depth, internal_format, + external_format, component_type, + nullptr, page_size, pages, usage)) { + if (level == 0) { + // If level 0 failed to create, this texture is useless. + success = false; + } + else if (_supports_texture_max_level) { + // Apparently, this is all it's going to get. + glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, level - 1); + } + break; + } + } + + // Spawn a task to do the upload asynchronously into the PBO. + if (image_ptr != nullptr) { + void *mapped_level_ptr = (char *)mapped_ptr + pbo_offset; + + if (ptimage) { + ptimage.node_ref(); + } + _async_chain->add([=, ptimage = std::move(ptimage), token = counter.make_token()](AsyncTask *task) mutable { + { + PStatTimer timer(_load_texture_copy_pcollector); + copy_image((unsigned char *)mapped_level_ptr, image_ptr, orig_view_size, + external_format, num_components, component_width); + } + if (ptimage) { + ptimage.node_unref(); + } + + token.complete(true); + return AsyncTask::DS_done; + }, "copy:" + tex->get_name()); + + if (async_levels == nullptr) { + async_levels = new AsyncLevel[num_levels + 1]; + } + async_levels[num_async_levels++] = {width, sub_height, depth, page_size, pbo_offset, std::move(pages)}; + pbo_offset += page_size * depth; + continue; + } + } +#endif + if (image_ptr != nullptr) { + if (page_size != orig_page_size || external_format == GL_RGBA || external_format == GL_RGB) { + // If the GL doesn't claim to support BGR, we may have to reverse + // the component ordering of the image. + PStatTimer timer(_load_texture_copy_pcollector); + PTA_uchar new_image = PTA_uchar::empty_array(page_size * depth); + copy_image(&new_image[0], image_ptr, orig_view_size, + external_format, num_components, component_width); + + ptimage = std::move(new_image); + } + } + + // Try updating the existing storage (sub-loading) first. + if (!needs_reload) { + if (!upload_texture_level(false, compressed, target, level, + width, sub_height, depth, internal_format, + external_format, component_type, + image_ptr, page_size, pages, usage)) { + break; + } + + // Did that fail? If it did, we'll immediately try again, this time + // loading the texture from scratch. + GLenum error_code = gl_get_error(); + if (error_code != GL_NO_ERROR) { + if (GLCAT.is_warning()) { + GLCAT.warning() + << "GL texture subload failed for " << tex->get_name() + << " level " << level << ": " << get_error_string(error_code) << "\n"; + } + needs_reload = true; + } + } + + if (needs_reload) { + if (!upload_texture_level(true, compressed, target, level, + width, height, depth, internal_format, + external_format, component_type, + image_ptr, page_size, pages, usage)) { + + if (_supports_texture_max_level) { + // Apparently, this is all it's going to get. + glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, level); + } + if (level == 0) { + success = false; + } + break; + } + } + } + + // Purely synchronous path, we can finish up the creation now. + // Report the error message explicitly if the GL texture creation failed. + if (needs_reload) { GLenum error_code = gl_get_error(); if (error_code != GL_NO_ERROR) { GLCAT.error() @@ -14847,23 +15005,349 @@ upload_texture_image(CLP(TextureContext) *gtc, int view, bool needs_reload, << " : " << get_error_string(error_code) << "\n"; gtc->_has_storage = false; - return false; + success = false; } } - if (gtc->_generate_mipmaps && _glGenerateMipmap != nullptr && !image.is_null()) { - // We uploaded an image; we may need to generate mipmaps. - if (GLCAT.is_debug()) { - GLCAT.debug() - << "generating mipmaps for texture " << tex->get_name() << " view " - << view << ", " << width << " x " << height << " x " << depth - << ", mipmaps = " << num_levels << "\n"; +#if defined(HAVE_THREADS) && !defined(OPENGLES_1) + if (async_levels != nullptr) { + // Schedule a follow-up task to finish the upload, which needs to happen + // with bound context, so we use a special mini job queue for that. + + // Storing 0 as last item saves some closure space. + async_levels[num_async_levels] = {0}; + + std::move(counter).then([ + this, token = std::move(token), + async_levels, gtc, view, pbo, pbo_size, upload_count, + external_format, component_type, compressed + ] (bool success) mutable { + call_later([=, token = std::move(token)] () mutable { + _glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + _glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + + if (gtc->_uploads_finished - upload_count >= 0) { + // Updates arrived out of order, so we skip this one, since a newer + // update was already finished. + GLCAT.info() + << "Discarding async update #" << upload_count << " to texture " + << gtc->get_texture()->get_name() << "\n"; + success = false; + } + else if (view >= gtc->_num_views) { + // If we uploaded a view that is no longer needed, we silently + // consider it a success, even if the task failed. + success = true; + } + else if (!apply_texture(gtc, view)) { + success = false; + } + else if (success) { + PStatTimer timer(_load_texture_pcollector); + + if (gtc->_target == GL_TEXTURE_BUFFER) { + // We can use a trick for buffer textures: just swap the "PBO" with + // the existing texture storage. The existing storage becomes the + // new PBO. Note also that buffer textures have no mipmaps. + _glTexBuffer(GL_TEXTURE_BUFFER, gtc->_internal_format, pbo); + std::swap(pbo, gtc->_buffers[view]); + } + else { + for (int level = 0; async_levels[level].width != 0; ++level) { + AsyncLevel &data = async_levels[level]; + if (!upload_texture_level(false, compressed, gtc->_target, level, + data.width, data.height, data.depth, + gtc->_internal_format, external_format, + component_type, (unsigned char *)data.pbo_offset, + data.page_size, data.pages, GL_STATIC_DRAW)) { + if (_supports_texture_max_level && !gtc->_generate_mipmaps) { + // Apparently, this is all it's going to get. + glTexParameteri(gtc->_target, GL_TEXTURE_MAX_LEVEL, level - 1); + } + success = false; + break; + } + } + } + + if (success) { + gtc->_uploads_finished = upload_count; + } + + if (gtc->_generate_mipmaps && _glGenerateMipmap != nullptr) { + // We uploaded an image; we may need to generate mipmaps. + if (GLCAT.is_debug()) { + GLCAT.debug() + << "generating mipmaps for texture " << gtc->get_texture()->get_name() + << " view " << view << ", " << async_levels[0].width << " x " + << async_levels[0].height << " x " << async_levels[0].depth + << ", mipmaps = " << gtc->_num_levels + << " (async update #" << upload_count << ")\n"; + } + _glGenerateMipmap(gtc->_target); + } + + if (success && gtc->get_texture()->get_post_load_store_cache()) { + if (!do_extract_texture_data(gtc, view)) { + success = false; + } + } + } + _glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + + // This goes back into the pool. + gtc->return_pbo(pbo, pbo_size); + + token.complete(success); + + delete[] async_levels; + }); + }); + } else +#endif + { + // This ensures any pending async op will not overwrite what we just did. + gtc->_uploads_finished = upload_count; + + if (pbo != 0) { + // For whatever reason, we haven't used the PBO, so return it. + gtc->return_pbo(pbo, pbo_size); } - _glGenerateMipmap(target); + + if (gtc->_generate_mipmaps && _glGenerateMipmap != nullptr && !image.is_null()) { + // We uploaded an image; we may need to generate mipmaps. + if (GLCAT.is_debug()) { + GLCAT.debug() + << "generating mipmaps for texture " << gtc->get_texture()->get_name() << " view " + << view << ", " << width << " x " << height << " x " << depth + << ", mipmaps = " << num_levels << "\n"; + } + _glGenerateMipmap(gtc->_target); + } + + if (gtc->get_texture()->get_post_load_store_cache()) { + if (!do_extract_texture_data(gtc, view)) { + success = false; + } + } + + token.complete(success); } report_my_gl_errors(); + return success; +} + +/** + * Performs the actual OpenGL call to update the texture data for the given + * mipmap level (be sure to subtract the mipmap_bias before passing it in). + * + * If full_reload is true, recreates the texture storage, otherwise subloads + * into the existing texture storage. A texture storage with undefined + * contents can be created by setting image_ptr to nullptr, in which case + * compressed must be false. + * + * Returns true if this texture format was supported, false otherwise. + */ +bool CLP(GraphicsStateGuardian):: +upload_texture_level(bool full_reload, bool compressed, GLenum target, + int level, int width, int height, int depth, + GLint internal_format, GLint external_format, + GLenum component_type, const unsigned char *image_ptr, + size_t page_size, SparseArray pages, + GLenum usage_hint) { + + switch (target) { +#ifndef OPENGLES_1 + case GL_TEXTURE_3D: + if (!_supports_3d_texture) { + return false; + } + + if (full_reload) { + if (!compressed) { + _glTexImage3D(target, level, internal_format, + width, height, depth, 0, + external_format, component_type, image_ptr); + } else { + _glCompressedTexImage3D(target, level, external_format, + width, height, depth, 0, page_size * depth, image_ptr); + } + } else { + for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) { + int begin = pages.get_subrange_begin(sri); + int num_pages = pages.get_subrange_end(sri) - begin; + const unsigned char *page_ptr = image_ptr + page_size * begin; + + if (!compressed) { + _glTexSubImage3D(target, level, + 0, 0, begin, width, height, num_pages, + external_format, component_type, page_ptr); + } else { + _glCompressedTexSubImage3D(target, level, + 0, 0, begin, width, height, num_pages, + external_format, + page_size * num_pages, page_ptr); + } + } + } + break; +#endif // OPENGLES_1 + +#ifndef OPENGLES_1 + case GL_TEXTURE_2D_ARRAY: + case GL_TEXTURE_CUBE_MAP_ARRAY: + if (!_supports_2d_texture_array) { + return false; + } + + if (full_reload) { + if (!compressed) { + _glTexImage3D(target, level, internal_format, width, height, depth, 0, + external_format, component_type, image_ptr); + } else { + _glCompressedTexImage3D(target, level, external_format, + width, height, depth, 0, + page_size * depth, image_ptr); + } + } else { + for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) { + int begin = pages.get_subrange_begin(sri); + int num_pages = pages.get_subrange_end(sri) - begin; + const unsigned char *page_ptr = image_ptr + page_size * begin; + + if (!compressed) { + _glTexSubImage3D(target, level, + 0, 0, begin, width, height, num_pages, + external_format, component_type, page_ptr); + } else { + _glCompressedTexSubImage3D(target, level, + 0, 0, begin, width, height, num_pages, + external_format, + page_size * num_pages, page_ptr); + } + } + } + break; +#endif // !OPENGLES_1 + +#ifndef OPENGLES_1 + case GL_TEXTURE_BUFFER: + if (!_supports_buffer_texture) { + return false; + } + + if (full_reload) { + _glBufferData(GL_TEXTURE_BUFFER, page_size, image_ptr, usage_hint); + } else { + _glBufferSubData(GL_TEXTURE_BUFFER, 0, page_size, image_ptr); + } + break; +#endif // !OPENGLES_1 + + case GL_TEXTURE_CUBE_MAP: + if (!_supports_cube_map) { + return false; + } + + // This is the only texture type that must be specified using separate + // per-page calls. + if (full_reload) { + for (int z = 0; z < 6; ++z) { + GLenum page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z; + const unsigned char *page_ptr = + (image_ptr != nullptr) ? image_ptr + page_size * z : nullptr; + + if (!compressed) { + glTexImage2D(page_target, level, internal_format, width, height, 0, + external_format, component_type, page_ptr); + } else { + _glCompressedTexImage2D(page_target, level, external_format, + width, height, 0, page_size, page_ptr); + } + } + } else { + for (int z = 0; z < 6; ++z) { + if (pages.get_bit(z)) { + GLenum page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z; + const unsigned char *page_ptr = image_ptr + page_size * z; + + if (!compressed) { + glTexSubImage2D(page_target, level, 0, 0, width, height, + external_format, component_type, page_ptr); + } else { + _glCompressedTexSubImage2D(page_target, level, + 0, 0, width, height, + external_format, page_size, page_ptr); + } + } + } + } + break; + +#ifndef OPENGLES + case GL_TEXTURE_1D: + if (full_reload) { + if (!compressed) { + glTexImage1D(target, level, internal_format, + width, 0, external_format, component_type, image_ptr); + } else { + _glCompressedTexImage1D(target, level, external_format, + width, 0, page_size, image_ptr); + } + } else { + if (!compressed) { + glTexSubImage1D(target, level, 0, width, + external_format, component_type, image_ptr); + } else { + _glCompressedTexSubImage1D(target, level, 0, width, + external_format, page_size, image_ptr); + } + } + break; +#endif // !OPENGLES + + default: + if (full_reload) { + if (!compressed) { + glTexImage2D(target, level, internal_format, width, height, 0, + external_format, component_type, image_ptr); + } else { + _glCompressedTexImage2D(target, level, external_format, + width, height, 0, page_size, image_ptr); + } + } else { + if (!compressed) { + glTexSubImage2D(target, level, 0, 0, width, height, + external_format, component_type, image_ptr); + } else { + _glCompressedTexSubImage2D(target, level, 0, 0, width, height, + external_format, page_size, image_ptr); + } + } + break; + } + +#ifdef DO_PSTATS + if (full_reload) { + _data_transferred_pcollector.add_level(page_size * depth); + } else { + _data_transferred_pcollector.add_level(pages.get_num_on_bits() * depth); + } +#endif + + // Did that fail? If it did, we'll immediately try again, this time + // loading the texture from scratch. + /*GLenum error_code = gl_get_error(); + if (error_code != GL_NO_ERROR) { + if (GLCAT.is_debug()) { + GLCAT.debug() + << "GL texture subload failed for " << tex->get_name() + << " : " << get_error_string(error_code) << "\n"; + } + full_reload = true; + }*/ return true; } @@ -16162,3 +16646,80 @@ do_issue_scissor() { } } } + +#ifndef OPENGLES_1 +/** + * Maps a buffer as write-only, discarding the previous contents. If + * create_storage is true, allocates new storage for the buffer. May use the + * given target to temporarily bind the buffer, if DSA is not supported. + */ +void *CLP(GraphicsStateGuardian):: +map_write_discard_buffer(GLenum target, GLuint buffer, size_t size, + bool create_storage) { + nassertr(buffer != 0, nullptr); + +#ifndef OPENGLES + if (!create_storage && _glMapNamedBufferRange != nullptr) { + return _glMapNamedBufferRange(buffer, 0, size, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT); + } +#endif + + _glBindBuffer(target, buffer); + + void *mapped_ptr; + if (_glMapBufferRange != nullptr) { + if (create_storage) { + if (_supports_buffer_storage) { + _glBufferStorage(target, size, nullptr, GL_MAP_WRITE_BIT); + } else { + _glBufferData(target, size, nullptr, GL_STATIC_DRAW); + } + } + mapped_ptr = _glMapBufferRange(target, 0, size, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT); + } else { +#ifdef OPENGLES + mapped_ptr = nullptr; +#else + // Explicitly orphan the buffer before mapping. + _glBufferData(target, size, nullptr, GL_STATIC_DRAW); + mapped_ptr = _glMapBuffer(target, GL_WRITE_ONLY); +#endif + } + + _glBindBuffer(target, 0); + return mapped_ptr; +} +#endif // !OPENGLES_1 + +/** + * Adds a job to the queue to be processed later while the context is bound, + * useful for calling from other threads. + */ +void CLP(GraphicsStateGuardian):: +call_later(Completable &&job) { + MutexHolder holder(_job_queue_mutex); + _job_queue.push_back(std::move(job)); + _job_queue_cvar.notify(); +} + +/** + * Processes any pending jobs from the queue. If wait is true, waits for at + * least one job if the queue is empty. + * + * May only be called on the draw thread. + */ +void CLP(GraphicsStateGuardian):: +process_pending_jobs(bool wait) { + JobQueue jobs; + { + MutexHolder holder(_job_queue_mutex); + if (wait && _job_queue.empty()) { + _job_queue_cvar.wait(); + } + _job_queue.swap(jobs); + } + + for (auto &job : jobs) { + std::move(job)(); + } +} diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index b8d1c02d30..cda382811a 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -39,6 +39,8 @@ #include "geomVertexArrayData.h" #include "lightMutex.h" #include "pStatGPUTimer.h" +#include "completionToken.h" +#include "asyncTaskChain.h" class PlaneNode; class Light; @@ -230,6 +232,7 @@ typedef void (APIENTRYP PFNGLGETPROGRAMBINARYPROC) (GLuint program, GLsizei bufS typedef void (APIENTRYP PFNGLPROGRAMBINARYPROC) (GLuint program, GLenum binaryFormat, const void *binary, GLsizei length); typedef void (APIENTRYP PFNGLGETINTERNALFORMATIVPROC) (GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint *params); typedef void (APIENTRYP PFNGLBUFFERSTORAGEPROC) (GLenum target, GLsizeiptr size, const void *data, GLbitfield flags); +typedef void (APIENTRYP PFNGLCOPYBUFFERSUBDATAPROC) (GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size); typedef void (APIENTRYP PFNGLBINDIMAGETEXTUREPROC) (GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format); typedef void (APIENTRYP PFNGLCLEARTEXIMAGEPROC) (GLuint texture, GLint level, GLenum format, GLenum type, const void *data); typedef void (APIENTRYP PFNGLCLEARTEXSUBIMAGEPROC) (GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void *data); @@ -345,7 +348,8 @@ public: #endif virtual TextureContext *prepare_texture(Texture *tex); - virtual bool update_texture(TextureContext *tc, bool force); + virtual bool update_texture(TextureContext *tc, bool force, + CompletionToken token = CompletionToken()); virtual void release_texture(TextureContext *tc); virtual void release_textures(const pvector &contexts); virtual bool extract_texture_data(Texture *tex); @@ -637,12 +641,21 @@ protected: bool apply_texture(CLP(TextureContext) *gtc, int view); bool apply_sampler(GLuint unit, const SamplerState &sampler, CLP(TextureContext) *gtc, int view); - bool upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps); - bool upload_texture_image(CLP(TextureContext) *gtc, int view, - bool needs_reload, int mipmap_bias, int num_levels, + bool upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps, + CompletionToken token = CompletionToken()); + bool upload_texture_view(CLP(TextureContext) *gtc, int view, + bool needs_reload, int mipmap_bias, int num_levels, + GLint internal_format, GLint external_format, + GLenum component_type, bool compressed, + int async_buffers, CompletionToken token); + bool upload_texture_level(bool full_reload, bool compressed, + GLenum target, int level, + int width, int height, int depth, GLint internal_format, GLint external_format, GLenum component_type, - Texture::CompressionMode image_compression); + const unsigned char *image_ptr, + size_t page_size, SparseArray pages, + GLenum usage_hint); void generate_mipmaps(CLP(TextureContext) *gtc); bool upload_simple_texture(CLP(TextureContext) *gtc); @@ -658,6 +671,14 @@ protected: void do_point_size(); #endif +#ifndef OPENGLES_1 + void *map_write_discard_buffer(GLenum target, GLuint buffer, size_t size, + bool create_storage); +#endif + + void call_later(Completable &&job); + void process_pending_jobs(bool wait); + enum AutoAntialiasMode { AA_poly, AA_line, @@ -904,6 +925,10 @@ public: PFNGLGETBUFFERSUBDATAPROC _glGetBufferSubData; #endif +#ifndef OPENGLES_1 + PFNGLCOPYBUFFERSUBDATAPROC _glCopyBufferSubData; +#endif + #ifdef OPENGLES PFNGLMAPBUFFERRANGEEXTPROC _glMapBufferRange; PFNGLUNMAPBUFFEROESPROC _glUnmapBuffer; @@ -911,6 +936,10 @@ public: PFNGLMAPBUFFERRANGEPROC _glMapBufferRange; #endif +#ifndef OPENGLES_1 + bool _supports_pixel_buffers; +#endif + #ifndef OPENGLES_1 bool _supports_uniform_buffers; bool _supports_shader_buffers; @@ -978,6 +1007,7 @@ public: PFNGLTEXTUREPARAMETERIPROC _glTextureParameteri; PFNGLGENERATETEXTUREMIPMAPPROC _glGenerateTextureMipmap; PFNGLBINDTEXTUREUNITPROC _glBindTextureUnit; + PFNGLMAPNAMEDBUFFERRANGEPROC _glMapNamedBufferRange; #endif #ifndef OPENGLES_1 @@ -1231,6 +1261,16 @@ public: }; pdeque _async_ram_copies; +#ifdef HAVE_THREADS + AsyncTaskChain *_async_chain; +#endif + + // Min job system pending a real job system + typedef pvector JobQueue; + Mutex _job_queue_mutex; + ConditionVar _job_queue_cvar; + JobQueue _job_queue; + BufferResidencyTracker _renderbuffer_residency; PStatCollector _active_ppbuffer_memory_pcollector; @@ -1274,6 +1314,7 @@ private: friend class CLP(BufferContext); friend class CLP(ShaderContext); friend class CLP(CgShaderContext); + friend class CLP(TextureContext); friend class CLP(GraphicsBuffer); friend class CLP(OcclusionQueryContext); }; diff --git a/panda/src/glstuff/glTextureContext_src.I b/panda/src/glstuff/glTextureContext_src.I index bde6a4d977..a0a986e80f 100644 --- a/panda/src/glstuff/glTextureContext_src.I +++ b/panda/src/glstuff/glTextureContext_src.I @@ -59,3 +59,43 @@ get_view_buffer(int view) const { return 0; } } + +/** + * Returns true if an async upload is pending. + */ +INLINE bool CLP(TextureContext):: +is_upload_pending() const { + // We can't simply compare _uploads_started to _uploads_finished, since + // they also get set to the same by cancel_pending_uploads() + return _uploads_pending > 0; +} + +/** + * Waits for all uploads to be finished. + */ +INLINE void CLP(TextureContext):: +wait_pending_uploads() const { + if (is_upload_pending()) { + do_wait_pending_uploads(); + } +} + +/** + * Cancels all asynchronous uploads. Not guaranteed to be cancelled by the + * time this returns, consider following this up with a call to + * wait_pending_uploads(). + */ +INLINE void CLP(TextureContext):: +cancel_pending_uploads() { + _uploads_finished = _uploads_started; +} + +/** + * Waits for an unused PBO unless we're not at the given limit of PBOs yet. + */ +INLINE void CLP(TextureContext):: +wait_for_unused_pbo(int limit) const { + if (_unused_pbos.empty() && _num_pbos >= limit) { + do_wait_for_unused_pbo(limit); + } +} diff --git a/panda/src/glstuff/glTextureContext_src.cxx b/panda/src/glstuff/glTextureContext_src.cxx index f3d3aaf18c..efc001df34 100644 --- a/panda/src/glstuff/glTextureContext_src.cxx +++ b/panda/src/glstuff/glTextureContext_src.cxx @@ -13,6 +13,8 @@ #include "pnotify.h" +static PStatCollector _wait_async_texture_uploads_pcollector("Wait:Async Texture Uploads"); + TypeHandle CLP(TextureContext)::_type_handle; /** @@ -48,6 +50,8 @@ evict_lru() { */ void CLP(TextureContext):: reset_data(GLenum target, int num_views) { + cancel_pending_uploads(); + // Free the texture resources. set_num_views(0); @@ -238,3 +242,59 @@ mark_incoherent(bool wrote) { } #endif // !OPENGLES_1 + +/** + * Returns a PBO with the given size to the pool of unused PBOs. + */ +void CLP(TextureContext):: +return_pbo(GLuint pbo, size_t size) { + // Also triggers when the number of buffers is -1 (which effectively means + // to always delete the buffers after use). + if (_num_pbos > get_texture()->get_num_async_transfer_buffers() || + size < _pbo_size) { + // We have too many PBOs, or this PBO is no longer of the proper + // size, so delete it rather than returning it to the pool. + _num_pbos--; + _glgsg->_glDeleteBuffers(1, &pbo); + } else { + _unused_pbos.push_front(pbo); + } +} + +/** + * Deletes all unused PBOs. + */ +void CLP(TextureContext):: +delete_unused_pbos() { + if (!_unused_pbos.empty()) { + for (GLuint pbo : _unused_pbos) { + _glgsg->_glDeleteBuffers(1, &pbo); + } + _num_pbos -= (int)_unused_pbos.size(); + _unused_pbos.clear(); + } +} + +/** + * Waits for all uploads to be finished. + */ +void CLP(TextureContext):: +do_wait_pending_uploads() const { + PStatTimer timer(_wait_async_texture_uploads_pcollector); + do { + _glgsg->process_pending_jobs(true); + } + while (is_upload_pending()); +} + +/** + * + */ +void CLP(TextureContext):: +do_wait_for_unused_pbo(int limit) const { + PStatTimer timer(_wait_async_texture_uploads_pcollector); + do { + _glgsg->process_pending_jobs(true); + } + while (_unused_pbos.empty() && _num_pbos >= limit); +} diff --git a/panda/src/glstuff/glTextureContext_src.h b/panda/src/glstuff/glTextureContext_src.h index addd03fede..03626a46ce 100644 --- a/panda/src/glstuff/glTextureContext_src.h +++ b/panda/src/glstuff/glTextureContext_src.h @@ -47,6 +47,18 @@ public: void mark_incoherent(bool wrote); #endif + INLINE bool is_upload_pending() const; + INLINE void wait_pending_uploads() const; + INLINE void cancel_pending_uploads(); + + void return_pbo(GLuint pbo, size_t size); + void delete_unused_pbos(); + INLINE void wait_for_unused_pbo(int limit) const; + +private: + void do_wait_pending_uploads() const; + void do_wait_for_unused_pbo(int limit) const; + private: // This is the GL "name" of the texture object. GLuint _index; @@ -76,6 +88,14 @@ public: GLenum _target; SamplerState _active_sampler; + // These counters are used to prevent out-of-order updates. + int _uploads_started = 0; + int _uploads_finished = 0; + int _uploads_pending = 0; + pdeque _unused_pbos; + int _num_pbos = 0; + size_t _pbo_size = 0; + CLP(GraphicsStateGuardian) *_glgsg; // These are set to the equivalent counter in glgsg when a write is performed. diff --git a/panda/src/glstuff/glmisc_src.cxx b/panda/src/glstuff/glmisc_src.cxx index f223ef266e..184b83523c 100644 --- a/panda/src/glstuff/glmisc_src.cxx +++ b/panda/src/glstuff/glmisc_src.cxx @@ -22,6 +22,11 @@ ConfigVariableBool gl_forward_compatible PRC_DESC("Setting this to true will request a forward-compatible OpenGL " "context, which will not support the fixed-function pipeline.")); +ConfigVariableBool gl_support_dsa + ("gl-support-dsa", true, + PRC_DESC("Configure this false if you suspect your GL's implementation of " + "Direct State Access is broken.")); + ConfigVariableBool gl_support_fbo ("gl-support-fbo", true, PRC_DESC("Configure this false if your GL's implementation of " diff --git a/panda/src/glstuff/glmisc_src.h b/panda/src/glstuff/glmisc_src.h index 1cc96726fe..7ee108d02e 100644 --- a/panda/src/glstuff/glmisc_src.h +++ b/panda/src/glstuff/glmisc_src.h @@ -35,6 +35,7 @@ extern EXPCL_GL ConfigVariableInt gl_version; extern EXPCL_GL ConfigVariableBool gl_forward_compatible; extern EXPCL_GL ConfigVariableBool gl_support_fbo; +extern ConfigVariableBool gl_support_dsa; extern ConfigVariableBool gl_cheap_textures; extern ConfigVariableBool gl_ignore_clamp; extern ConfigVariableBool gl_support_clamp_to_border; diff --git a/panda/src/gobj/preparedGraphicsObjects.cxx b/panda/src/gobj/preparedGraphicsObjects.cxx index b76b052476..3089537dd2 100644 --- a/panda/src/gobj/preparedGraphicsObjects.cxx +++ b/panda/src/gobj/preparedGraphicsObjects.cxx @@ -1515,9 +1515,24 @@ begin_frame(GraphicsStateGuardianBase *gsg, Thread *current_thread) { Texture *tex = qti->first; TextureContext *tc = tex->prepare_now(this, gsg); if (tc != nullptr) { - gsg->update_texture(tc, true); - if (qti->second != nullptr) { - qti->second->set_result(tc); + if (tex->get_num_async_transfer_buffers() == 0) { + gsg->update_texture(tc, true); + if (qti->second != nullptr) { + qti->second->set_result(tc); + } + } else { + // Async update + CompletionToken token; + if (qti->second != nullptr) { + token = [tc, fut = std::move(qti->second)] (bool success) { + if (success) { + fut->set_result(tc); + } else { + fut->notify_removed(); + } + }; + } + gsg->update_texture(tc, false, std::move(token)); } } } diff --git a/panda/src/gobj/texture.I b/panda/src/gobj/texture.I index 0d349f6653..d304bfbfed 100644 --- a/panda/src/gobj/texture.I +++ b/panda/src/gobj/texture.I @@ -2139,6 +2139,14 @@ rescale_texture() { return do_rescale_texture(cdata); } +/** + * Returns the number previously passed to setup_async_transfer(). + */ +INLINE int Texture:: +get_num_async_transfer_buffers() const { + return _num_async_transfer_buffers.load(std::memory_order_relaxed); +} + /** * Works like adjust_size, but also considers the texture class. Movie * textures, for instance, always pad outwards, regardless of textures- diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index 2afe0f0320..b8b1f5bbf1 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -1570,6 +1570,27 @@ get_view_modified_pages(UpdateSeq since, int view, int n) const { return result; } +/** + * Sets the number of buffers for asynchronous upload of texture data. If this + * number is higher than 0, future texture uploads will occur in the background, + * up to the provided amount at a time. The asynchronous upload will be + * triggered by calls to prepare() or when the texture comes into view and + * allow-incomplete-render is true. + * + * Each buffer is only large enough to contain a single view, so you may wish + * to create twice as many buffers if you want to update twice as many views. + * + * You can also pass the special value -1, which means to create as many + * buffers as is necessary for all asynchronous uploads to take place, and they + * will be deleted afterwards automatically. + * + * This setting will take effect immediately. + */ +void Texture:: +setup_async_transfer(int num_buffers) { + _num_async_transfer_buffers.store(num_buffers); +} + /** * Indicates that the texture should be enqueued to be prepared in the * indicated prepared_objects at the beginning of the next frame. This will @@ -5704,7 +5725,14 @@ do_modify_ram_image(CData *cdata) { } else { do_clear_ram_mipmap_images(cdata); } - return cdata->_ram_images[0]._image; + PTA_uchar data = cdata->_ram_images[0]._image; + if (data.get_node_ref_count() > 0) { + // Copy on write, if an upload thread is reading this now. + PTA_uchar new_data = PTA_uchar::empty_array(0); + new_data.v() = data.v(); + data.swap(new_data); + } + return data; } /** @@ -5779,7 +5807,15 @@ do_modify_ram_mipmap_image(CData *cdata, int n) { cdata->_ram_images[n]._image.empty()) { do_make_ram_mipmap_image(cdata, n); } - return cdata->_ram_images[n]._image; + + PTA_uchar data = cdata->_ram_images[n]._image; + if (data.get_node_ref_count() > 0) { + // Copy on write, if an upload thread is reading this now. + PTA_uchar new_data = PTA_uchar::empty_array(0); + new_data.v() = data.v(); + data.swap(new_data); + } + return data; } /** diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index f6ec5eb1f2..4ba2b58169 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -47,6 +47,7 @@ #include "pfmFile.h" #include "asyncTask.h" #include "extension.h" +#include "patomic.h" class TextureContext; class FactoryParams; @@ -536,6 +537,8 @@ PUBLISHED: MAKE_PROPERTY(auto_texture_scale, get_auto_texture_scale, set_auto_texture_scale); + void setup_async_transfer(int num_buffers); + PT(AsyncFuture) prepare(PreparedGraphicsObjects *prepared_objects); bool is_prepared(PreparedGraphicsObjects *prepared_objects) const; bool was_image_modified(PreparedGraphicsObjects *prepared_objects) const; @@ -628,6 +631,7 @@ PUBLISHED: public: void texture_uploaded(); + INLINE int get_num_async_transfer_buffers() const; virtual bool has_cull_callback() const; virtual bool cull_callback(CullTraverser *trav, const CullTraverserData &data) const; @@ -1072,6 +1076,8 @@ protected: typedef pmap Contexts; Contexts _contexts; + patomic_signed_lock_free _num_async_transfer_buffers { 0 }; + // It is common, when using normal maps, specular maps, gloss maps, and // such, to use a file naming convention where the filenames of the special // maps are derived by concatenating a suffix to the name of the diffuse diff --git a/panda/src/gsgbase/graphicsStateGuardianBase.h b/panda/src/gsgbase/graphicsStateGuardianBase.h index 80d31f5c3c..d49f144046 100644 --- a/panda/src/gsgbase/graphicsStateGuardianBase.h +++ b/panda/src/gsgbase/graphicsStateGuardianBase.h @@ -22,6 +22,7 @@ #include "lightMutex.h" #include "patomic.h" #include "small_vector.h" +#include "completionToken.h" // A handful of forward references. @@ -149,6 +150,7 @@ public: virtual TextureContext *prepare_texture(Texture *tex)=0; virtual bool update_texture(TextureContext *tc, bool force)=0; + virtual bool update_texture(TextureContext *tc, bool force, CompletionToken token)=0; virtual void release_texture(TextureContext *tc)=0; virtual void release_textures(const pvector &contexts)=0; virtual bool extract_texture_data(Texture *tex)=0; diff --git a/panda/src/putil/completable.I b/panda/src/putil/completable.I index 4f44471615..96d140be33 100644 --- a/panda/src/putil/completable.I +++ b/panda/src/putil/completable.I @@ -37,6 +37,20 @@ Completable(Completable &&from) noexcept : from._data = nullptr; } +/** + * + */ +INLINE Completable &Completable:: +operator =(Completable &&from) { + Data *data = _data; + _data = from._data; + from._data = nullptr; + if (data != nullptr) { + data->_function.load(std::memory_order_relaxed)(data, false); + } + return *this; +} + /** * */ diff --git a/panda/src/putil/completable.h b/panda/src/putil/completable.h index a184302d9c..9b0f6fdd12 100644 --- a/panda/src/putil/completable.h +++ b/panda/src/putil/completable.h @@ -32,6 +32,9 @@ public: INLINE Completable(const Completable ©) = delete; INLINE Completable(Completable &&from) noexcept; + INLINE Completable &operator =(const Completable ©) = delete; + INLINE Completable &operator =(Completable &&from); + INLINE void operator ()(); INLINE ~Completable(); From 55a3a0d2af41db31a937f960dcc84d58a694dbe4 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 27 Jan 2025 17:26:17 +0100 Subject: [PATCH 033/291] glgsg: async screenshot refactor, general mechanism for fences --- .../glstuff/glGraphicsStateGuardian_src.cxx | 220 ++++++++++-------- .../src/glstuff/glGraphicsStateGuardian_src.h | 20 +- 2 files changed, 134 insertions(+), 106 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 8317a326e9..1d3315aa52 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -4496,9 +4496,11 @@ begin_frame(Thread *current_thread) { _primitive_batches_display_list_pcollector.clear_level(); #endif - if (!_async_ram_copies.empty()) { - finish_async_framebuffer_ram_copies(); +#ifndef OPENGLES_1 + if (!_fences.empty()) { + process_fences(false); } +#endif #if defined(DO_PSTATS) && !defined(OPENGLES) int frame_number = ClockObject::get_global_clock()->get_frame_count(current_thread); @@ -8480,9 +8482,46 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, _glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT); } #endif - GLsync fence = _glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - _async_ram_copies.push_back({request, pbo, fence, external_format, - view, mapped_ptr, image_size}); + + insert_fence([ + this, request = PT(ScreenshotRequest)(request), + mapped_ptr, size = image_size, + pbo, view, external_format + ] (bool success) { + + void *ptr = mapped_ptr; + if (ptr == nullptr) { + ptr = map_read_buffer(GL_PIXEL_PACK_BUFFER, pbo, size); + } + + // Do the memcpy in the background, since it can be slow. + auto func = [=](AsyncTask *task) { + const unsigned char *result = (unsigned char *)ptr; + PTA_uchar new_image; + if (external_format == GL_RGBA || external_format == GL_RGB) { + // We may have to reverse the byte ordering of the image if GL didn't do + // it for us. + result = fix_component_ordering(new_image, result, size, + external_format, request->get_result()); + } + request->set_view_data(view, result); + + // Finishing can take a long time, release the client buffer first so it + // can be reused for the next screenshot. + this->release_client_buffer(pbo, ptr, size); + request->finish(); + return AsyncTask::DS_done; + }; +#ifdef HAVE_THREADS + // We assign a sort value based on the originating frame number, so that + // earlier frames will be processed before subsequent frames, but we don't + // make it unique for every frame, which would kill concurrency. + int frame_number = request->get_frame_number(); + _async_chain->add(std::move(func), "screenshot", frame_number >> 3, -(frame_number & ((1 << 3) - 1))); +#else + func(nullptr); +#endif + }); } else #endif if (external_format == GL_RGBA || external_format == GL_RGB) { @@ -8507,97 +8546,6 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, return true; } -/** - * Finishes all asynchronous framebuffer-copy-to-ram operations. - */ -void CLP(GraphicsStateGuardian):: -finish_async_framebuffer_ram_copies(bool force) { -#ifndef OPENGLES_1 - if (_async_ram_copies.empty()) { - return; - } - - PStatTimer timer(_copy_texture_finish_pcollector); - - if (force) { - // Just wait for the last fence, the rest must be complete too then. - PStatTimer timer(_wait_fence_pcollector); - GLsync fence = _async_ram_copies.back()._fence; - _glClientWaitSync(fence, 0, (GLuint64)-1); - } - - while (!_async_ram_copies.empty()) { - AsyncRamCopy © = _async_ram_copies.front(); - if (!force) { - GLenum result = _glClientWaitSync(copy._fence, 0, 0); - if (result != GL_ALREADY_SIGNALED && result != GL_CONDITION_SATISFIED) { - // Not yet done. The rest must not yet be done then, either. - break; - } - } - _glDeleteSync(copy._fence); - - GLuint pbo = copy._pbo; - int view = copy._view; - PT(ScreenshotRequest) request = std::move(copy._request); - GLuint external_format = copy._external_format; - void *mapped_ptr = copy._mapped_pointer; - size_t size = copy._size; - - if (mapped_ptr == nullptr) { - _glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo); -#ifdef OPENGLES - // There is neither glMapBuffer nor persistent mapping in OpenGL ES - mapped_ptr = _glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, size, GL_MAP_READ_BIT); -#else - // If we get here in desktop GL, we must not have persistent mapping - nassertv(!_supports_buffer_storage); - mapped_ptr = _glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY); -#endif - } - - // Do the memcpy in the background, since it can be slow. - auto func = [=](AsyncTask *task) { - const unsigned char *result = (unsigned char *)mapped_ptr; - PTA_uchar new_image; - if (external_format == GL_RGBA || external_format == GL_RGB) { - // We may have to reverse the byte ordering of the image if GL didn't do - // it for us. - result = fix_component_ordering(new_image, result, size, - external_format, request->get_result()); - } - request->set_view_data(view, result); - - // Finishing can take a long time, release the client buffer first so it - // can be reused for the next screenshot. - this->release_client_buffer(pbo, mapped_ptr, size); - request->finish(); - return AsyncTask::DS_done; - }; -#ifdef HAVE_THREADS - // We assign a sort value based on the originating frame number, so that - // earlier frames will be processed before subsequent frames, but we don't - // make it unique for every frame, which would kill concurrency. - int frame_number = request->get_frame_number(); - _async_chain->add(std::move(func), "screenshot", frame_number >> 3, -(frame_number & ((1 << 3) - 1))); -#else - func(nullptr); -#endif - - _async_ram_copies.pop_front(); - - // If there is 1 remaining, save it for next frame. This helps prevent an - // inconsistent frame rate when the number of fetched frames alternates - // between 0 and 2, which can settle into a stable feedback loop. - if (!force && _async_ram_copies.size() == 1) { - break; - } - } - - _glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); -#endif -} - #ifdef SUPPORT_FIXED_FUNCTION /** * @@ -16648,6 +16596,34 @@ do_issue_scissor() { } #ifndef OPENGLES_1 +/** + * Maps a buffer for reading. May be temporarily bound to the given target. + */ +void *CLP(GraphicsStateGuardian):: +map_read_buffer(GLenum target, GLuint buffer, size_t size) { + nassertr(buffer != 0, nullptr); + +#ifndef OPENGLES + if (_glMapNamedBufferRange != nullptr) { + return _glMapNamedBufferRange(buffer, 0, size, GL_MAP_READ_BIT); + } +#endif + + void *mapped_ptr = nullptr; + + _glBindBuffer(target, buffer); +#ifdef OPENGLES + // There is neither glMapBuffer nor persistent mapping in OpenGL ES + mapped_ptr = _glMapBufferRange(target, 0, size, GL_MAP_READ_BIT); +#else + // If we get here in desktop GL, we must not have persistent mapping + mapped_ptr = _glMapBuffer(target, GL_READ_ONLY); +#endif + + _glBindBuffer(target, 0); + return mapped_ptr; +} + /** * Maps a buffer as write-only, discarding the previous contents. If * create_storage is true, allocates new storage for the buffer. May use the @@ -16691,6 +16667,58 @@ map_write_discard_buffer(GLenum target, GLuint buffer, size_t size, } #endif // !OPENGLES_1 +#ifndef OPENGLES_1 +/** + * Inserts a fence into the command stream. + */ +void CLP(GraphicsStateGuardian):: +insert_fence(CompletionToken &&callback) { + GLsync fence = _glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + _fences.push_back({fence, std::move(callback)}); +} + +/** + * Checks which fences are finished and processes those. + */ +void CLP(GraphicsStateGuardian):: +process_fences(bool force) { + if (_fences.empty()) { + return; + } + + PStatTimer timer(_copy_texture_finish_pcollector); + + if (force) { + // Just wait for the last fence, the rest must be complete too then. + PStatTimer timer(_wait_fence_pcollector); + GLsync fence = _fences.back()._object; + _glClientWaitSync(fence, 0, (GLuint64)-1); + } + + while (!_fences.empty()) { + Fence &fence = _fences.front(); + if (!force) { + GLenum result = _glClientWaitSync(fence._object, 0, 0); + if (result != GL_ALREADY_SIGNALED && result != GL_CONDITION_SATISFIED) { + // Not yet done. The rest must not yet be done then, either. + break; + } + } + _glDeleteSync(fence._object); + + std::move(fence._token).complete(true); + _fences.pop_front(); + + // If there is 1 remaining, save it for next frame. This helps prevent an + // inconsistent frame rate when the number of fetched frames alternates + // between 0 and 2, which can settle into a stable feedback loop. + if (!force && _fences.size() == 1) { + break; + } + } +} +#endif // !OPENGLES_1 + /** * Adds a job to the queue to be processed later while the context is bound, * useful for calling from other threads. diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index cda382811a..a355962005 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -423,7 +423,6 @@ public: virtual bool framebuffer_copy_to_ram (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb, ScreenshotRequest *request); - void finish_async_framebuffer_ram_copies(bool force = false); #ifdef SUPPORT_FIXED_FUNCTION void apply_fog(Fog *fog); @@ -672,10 +671,16 @@ protected: #endif #ifndef OPENGLES_1 + void *map_read_buffer(GLenum target, GLuint buffer, size_t size); void *map_write_discard_buffer(GLenum target, GLuint buffer, size_t size, bool create_storage); #endif +#ifndef OPENGLES_1 + void insert_fence(CompletionToken &&callback); + void process_fences(bool force); +#endif + void call_later(Completable &&job); void process_pending_jobs(bool wait); @@ -1250,16 +1255,11 @@ public: FrameTiming *_current_frame_timing = nullptr; #endif - struct AsyncRamCopy { - PT(ScreenshotRequest) _request; - GLuint _pbo; - GLsync _fence; - GLuint _external_format; - int _view; - void *_mapped_pointer; - size_t _size; + struct Fence { + GLsync _object; + CompletionToken _token; }; - pdeque _async_ram_copies; + pdeque _fences; #ifdef HAVE_THREADS AsyncTaskChain *_async_chain; From 1a431b3894933df75bae4cc5a26fe10bbb3352ec Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 28 Jan 2025 09:53:34 +0100 Subject: [PATCH 034/291] glgsg: make async texture transfer threads configurable --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 6 +++--- panda/src/glstuff/glmisc_src.cxx | 13 +++++++++++++ panda/src/glstuff/glmisc_src.h | 3 +++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 1d3315aa52..b29387463b 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -715,10 +715,10 @@ CLP(GraphicsStateGuardian)(GraphicsEngine *engine, GraphicsPipe *pipe) : #endif #ifdef HAVE_THREADS - //XXX having a fixed number of threads is not a great idea. We ought to have - // a common thread pool that is sized based on the available number of CPUs. AsyncTaskManager *task_mgr = AsyncTaskManager::get_global_ptr(); - _async_chain = task_mgr->make_task_chain("texture_copy", 2, TP_normal); + _async_chain = task_mgr->make_task_chain("gl_texture_transfer", + gl_texture_transfer_num_threads, + gl_texture_transfer_thread_priority); #endif #ifdef DO_PSTATS diff --git a/panda/src/glstuff/glmisc_src.cxx b/panda/src/glstuff/glmisc_src.cxx index 184b83523c..0cc5326a15 100644 --- a/panda/src/glstuff/glmisc_src.cxx +++ b/panda/src/glstuff/glmisc_src.cxx @@ -326,6 +326,19 @@ ConfigVariableBool gl_depth_zero_to_one "range from 0 to 1, matching other graphics APIs. This setting " "requires OpenGL 4.5, or NVIDIA GeForce 8+ hardware.")); +ConfigVariableInt gl_texture_reload_num_threads + ("gl-texture-transfer-num-threads", 2, + PRC_DESC("The number of threads that will be started to upload and download " + "texture data asynchronously, either via the setup_async_transfer " + "interface on the the Texture class or via the async screenshot " + "interface.")); + +ConfigVariableEnum gl_texture_transfer_thread_priority + ("gl-texture-transfer-thread-priority", TP_normal, + PRC_DESC("The default thread priority to assign to the threads created for " + "asynchronous texture transfers. The default is 'normal'; you may " + "also specify 'low', 'high', or 'urgent'.")); + extern ConfigVariableBool gl_parallel_arrays; void CLP(init_classes)() { diff --git a/panda/src/glstuff/glmisc_src.h b/panda/src/glstuff/glmisc_src.h index 7ee108d02e..deb219dc5a 100644 --- a/panda/src/glstuff/glmisc_src.h +++ b/panda/src/glstuff/glmisc_src.h @@ -17,6 +17,7 @@ #include "configVariableEnum.h" #include "geomEnums.h" #include "coordinateSystem.h" +#include "threadPriority.h" // Define some macros to transparently map to the double or float versions of // the OpenGL function names. @@ -76,6 +77,8 @@ extern ConfigVariableBool gl_support_shadow_filter; extern ConfigVariableBool gl_support_vertex_array_bgra; extern ConfigVariableBool gl_force_image_bindings_writeonly; extern ConfigVariableEnum gl_coordinate_system; +extern ConfigVariableInt gl_texture_transfer_num_threads; +extern ConfigVariableEnum gl_texture_transfer_thread_priority; extern EXPCL_GL void CLP(init_classes)(); From 3725a00fdc65ff839c95f47e3852f1ba374bd444 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 28 Jan 2025 10:40:43 +0100 Subject: [PATCH 035/291] glgsg: Fix typo --- panda/src/glstuff/glmisc_src.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/glstuff/glmisc_src.cxx b/panda/src/glstuff/glmisc_src.cxx index 0cc5326a15..d1ae931c7c 100644 --- a/panda/src/glstuff/glmisc_src.cxx +++ b/panda/src/glstuff/glmisc_src.cxx @@ -326,7 +326,7 @@ ConfigVariableBool gl_depth_zero_to_one "range from 0 to 1, matching other graphics APIs. This setting " "requires OpenGL 4.5, or NVIDIA GeForce 8+ hardware.")); -ConfigVariableInt gl_texture_reload_num_threads +ConfigVariableInt gl_texture_transfer_num_threads ("gl-texture-transfer-num-threads", 2, PRC_DESC("The number of threads that will be started to upload and download " "texture data asynchronously, either via the setup_async_transfer " From 97fcbc2e15c8dd4b9c08156708b2c9e6e75f4f45 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Jan 2025 11:06:57 +0100 Subject: [PATCH 036/291] glgsg: Fix error message for unhandled external formats --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index b29387463b..3a74f56331 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -465,7 +465,10 @@ get_external_format_components(GLint external_format) { case GL_GREEN_INTEGER: case GL_BLUE_INTEGER: case GL_ALPHA_INTEGER: + case GL_STENCIL_INDEX: #endif + case GL_DEPTH_COMPONENT: + case GL_DEPTH_STENCIL: return 1; #ifndef OPENGLES_1 @@ -523,7 +526,10 @@ copy_image(unsigned char *new_image, const unsigned char *orig_image, case GL_GREEN_INTEGER: case GL_BLUE_INTEGER: case GL_ALPHA_INTEGER: + case GL_STENCIL_INDEX: #endif + case GL_DEPTH_COMPONENT: + case GL_DEPTH_STENCIL: if (num_components == 1) { memcpy(new_image, orig_image, orig_image_size); return; From 9c7e9e9805d8379814997d46e5391778c322c7a4 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Jan 2025 11:10:42 +0100 Subject: [PATCH 037/291] glgsg: Add more debug prints for renderbuffer creation --- panda/src/glstuff/glGraphicsBuffer_src.cxx | 35 ++++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/panda/src/glstuff/glGraphicsBuffer_src.cxx b/panda/src/glstuff/glGraphicsBuffer_src.cxx index a97818448d..b2d7b4a33a 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.cxx +++ b/panda/src/glstuff/glGraphicsBuffer_src.cxx @@ -1137,7 +1137,9 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot, if (slot == RTP_depth_stencil) { if (GLCAT.is_debug()) { - GLCAT.debug() << "Creating depth stencil renderbuffer.\n"; + GLCAT.debug() + << "Creating depth stencil renderbuffer with format 0x" << std::hex + << gl_format << std::dec << ".\n"; } // Allocate renderbuffer storage for depth stencil. GLint depth_size = 0, stencil_size = 0; @@ -1165,7 +1167,9 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot, } else if (slot == RTP_depth) { if (GLCAT.is_debug()) { - GLCAT.debug() << "Creating depth renderbuffer.\n"; + GLCAT.debug() + << "Creating depth renderbuffer with format 0x" << std::hex + << gl_format << std::dec << ".\n"; } // Allocate renderbuffer storage for regular depth. GLint depth_size = 0; @@ -1181,6 +1185,11 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot, } else { gl_format = GL_DEPTH_COMPONENT32F_NV; } + if (GLCAT.is_debug()) { + GLCAT.debug() + << "GL_DEPTH_COMPONENT32 not supported, switching to format 0x" + << std::hex << gl_format << std::dec << " instead.\n"; + } glgsg->_glRenderbufferStorage(GL_RENDERBUFFER_EXT, gl_format, _rb_size_x, _rb_size_y); glgsg->_glGetRenderbufferParameteriv(GL_RENDERBUFFER_EXT, GL_RENDERBUFFER_DEPTH_SIZE_EXT, &depth_size); @@ -1205,7 +1214,9 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot, } else { if (GLCAT.is_debug()) { - GLCAT.debug() << "Creating color renderbuffer.\n"; + GLCAT.debug() + << "Creating color renderbuffer with format 0x" << std::hex + << gl_format << std::dec << ".\n"; } glgsg->_glRenderbufferStorage(GL_RENDERBUFFER_EXT, gl_format, _rb_size_x, _rb_size_y); @@ -1253,6 +1264,12 @@ bind_slot_multisample(bool rb_resize, Texture **attach, RenderTexturePlane slot, #ifndef OPENGLES_2 if (_use_depth_stencil) { glgsg->_glBindRenderbuffer(GL_RENDERBUFFER_EXT, _rbm[slot]); + if (GLCAT.is_debug()) { + GLCAT.debug() + << "Creating depth stencil renderbuffer with format 0x" << std::hex + << GL_DEPTH_STENCIL_EXT << std::dec << " and " << _requested_multisamples + << " multisamples.\n"; + } if (_requested_coverage_samples) { glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples, _requested_multisamples, GL_DEPTH_STENCIL_EXT, @@ -1298,6 +1315,12 @@ bind_slot_multisample(bool rb_resize, Texture **attach, RenderTexturePlane slot, break; } } + if (GLCAT.is_debug()) { + GLCAT.debug() + << "Creating depth renderbuffer with format 0x" << std::hex + << format << std::dec << " and " << _requested_multisamples + << " multisamples.\n"; + } if (_requested_coverage_samples) { glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples, _requested_multisamples, format, @@ -1351,6 +1374,12 @@ bind_slot_multisample(bool rb_resize, Texture **attach, RenderTexturePlane slot, break; } #endif + if (GLCAT.is_debug()) { + GLCAT.debug() + << "Creating color renderbuffer with format 0x" << std::hex + << gl_format << std::dec << " and " << _requested_multisamples + << " multisamples.\n"; + } glgsg->_glBindRenderbuffer(GL_RENDERBUFFER_EXT, _rbm[slot]); if (_requested_coverage_samples) { glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples, From 1234388de8fdbace87c9e7765a979ba51f3b4b1f Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Jan 2025 11:11:47 +0100 Subject: [PATCH 038/291] glgsg: Make multisample renderbuffer format match regular one --- panda/src/glstuff/glGraphicsBuffer_src.cxx | 115 +++++++++++++++++++-- 1 file changed, 105 insertions(+), 10 deletions(-) diff --git a/panda/src/glstuff/glGraphicsBuffer_src.cxx b/panda/src/glstuff/glGraphicsBuffer_src.cxx index b2d7b4a33a..722e5091cf 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.cxx +++ b/panda/src/glstuff/glGraphicsBuffer_src.cxx @@ -1264,18 +1264,33 @@ bind_slot_multisample(bool rb_resize, Texture **attach, RenderTexturePlane slot, #ifndef OPENGLES_2 if (_use_depth_stencil) { glgsg->_glBindRenderbuffer(GL_RENDERBUFFER_EXT, _rbm[slot]); + GLuint format; +#ifdef OPENGLES_1 + format = GL_DEPTH24_STENCIL8_OES; +#else + if (_fb_properties.get_depth_bits() > 24 || + _fb_properties.get_float_depth()) { + if (!glgsg->_use_remapped_depth_range) { + format = GL_DEPTH32F_STENCIL8; + } else { + format = GL_DEPTH32F_STENCIL8_NV; + } + } else { + format = GL_DEPTH24_STENCIL8; + } +#endif if (GLCAT.is_debug()) { GLCAT.debug() << "Creating depth stencil renderbuffer with format 0x" << std::hex - << GL_DEPTH_STENCIL_EXT << std::dec << " and " << _requested_multisamples + << format << std::dec << " and " << _requested_multisamples << " multisamples.\n"; } if (_requested_coverage_samples) { glgsg->_glRenderbufferStorageMultisampleCoverage(GL_RENDERBUFFER_EXT, _requested_coverage_samples, - _requested_multisamples, GL_DEPTH_STENCIL_EXT, + _requested_multisamples, format, _rb_size_x, _rb_size_y); } else { - glgsg->_glRenderbufferStorageMultisample(GL_RENDERBUFFER_EXT, _requested_multisamples, GL_DEPTH_STENCIL_EXT, + glgsg->_glRenderbufferStorageMultisample(GL_RENDERBUFFER_EXT, _requested_multisamples, format, _rb_size_x, _rb_size_y); } #ifndef OPENGLES @@ -1314,6 +1329,16 @@ bind_slot_multisample(bool rb_resize, Texture **attach, RenderTexturePlane slot, default: break; } +#ifndef OPENGLES + } else if (_fb_properties.get_depth_bits() > 24) { + format = GL_DEPTH_COMPONENT32; + } else if (_fb_properties.get_depth_bits() > 16) { + format = GL_DEPTH_COMPONENT24; + } else if (_fb_properties.get_depth_bits() > 1) { + format = GL_DEPTH_COMPONENT16; + } else { + format = GL_DEPTH_COMPONENT; +#endif } if (GLCAT.is_debug()) { GLCAT.debug() @@ -1359,17 +1384,87 @@ bind_slot_multisample(bool rb_resize, Texture **attach, RenderTexturePlane slot, case RTP_aux_rgba_1: case RTP_aux_rgba_2: case RTP_aux_rgba_3: + gl_format = GL_RGBA; + break; default: - if (_fb_properties.get_srgb_color()) { - gl_format = GL_SRGB8_ALPHA8; - } else if (_fb_properties.get_float_color()) { - if (_fb_properties.get_color_bits() > 16 * 3) { - gl_format = GL_RGBA32F_ARB; + if (_fb_properties.get_alpha_bits() == 0) { + if (_fb_properties.get_srgb_color()) { + gl_format = GL_SRGB8; + } else if (_fb_properties.get_color_bits() > 16 * 3 || + _fb_properties.get_red_bits() > 16 || + _fb_properties.get_green_bits() > 16 || + _fb_properties.get_blue_bits() > 16) { + // 32-bit, which is always floating-point. + if (_fb_properties.get_blue_bits() > 0 || + _fb_properties.get_color_bits() == 1 || + _fb_properties.get_color_bits() > 32 * 2) { + gl_format = GL_RGB32F; + } else if (_fb_properties.get_green_bits() > 0 || + _fb_properties.get_color_bits() > 32) { + gl_format = GL_RG32F; + } else { + gl_format = GL_R32F; + } + } else if (_fb_properties.get_float_color()) { + // 16-bit floating-point. + if (_fb_properties.get_blue_bits() > 10 || + _fb_properties.get_color_bits() == 1 || + _fb_properties.get_color_bits() > 32) { + gl_format = GL_RGB16F; + } else if (_fb_properties.get_blue_bits() > 0) { + if (_fb_properties.get_red_bits() > 11 || + _fb_properties.get_green_bits() > 11) { + gl_format = GL_RGB16F; + } else { + gl_format = GL_R11F_G11F_B10F; + } + } else if (_fb_properties.get_green_bits() > 0 || + _fb_properties.get_color_bits() > 16) { + gl_format = GL_RG16F; + } else { + gl_format = GL_R16F; + } + } else if (_fb_properties.get_color_bits() > 10 * 3 || + _fb_properties.get_red_bits() > 10 || + _fb_properties.get_green_bits() > 10 || + _fb_properties.get_blue_bits() > 10) { + // 16-bit normalized. + if (_fb_properties.get_blue_bits() > 0 || + _fb_properties.get_color_bits() == 1 || + _fb_properties.get_color_bits() > 16 * 2) { + gl_format = GL_RGBA16; + } else if (_fb_properties.get_green_bits() > 0 || + _fb_properties.get_color_bits() > 16) { + gl_format = GL_RG16; + } else { + gl_format = GL_R16; + } + } else if (_fb_properties.get_color_bits() > 8 * 3 || + _fb_properties.get_red_bits() > 8 || + _fb_properties.get_green_bits() > 8 || + _fb_properties.get_blue_bits() > 8) { + gl_format = GL_RGB10_A2; } else { - gl_format = GL_RGBA16F_ARB; + gl_format = GL_RGB; } } else { - gl_format = GL_RGBA; + if (_fb_properties.get_srgb_color()) { + gl_format = GL_SRGB8_ALPHA8; + } else if (_fb_properties.get_float_color()) { + if (_fb_properties.get_color_bits() > 16 * 3) { + gl_format = GL_RGBA32F_ARB; + } else { + gl_format = GL_RGBA16F_ARB; + } + } else { + if (_fb_properties.get_color_bits() > 16 * 3) { + gl_format = GL_RGBA32F_ARB; + } else if (_fb_properties.get_color_bits() > 8 * 3) { + gl_format = GL_RGBA16; + } else { + gl_format = GL_RGBA; + } + } } break; } From fa64a52fca875b99c1cdb423968bb53985afec7b Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Jan 2025 11:12:07 +0100 Subject: [PATCH 039/291] glgsg: Do not prefer depth-stencil ms renderbuffers on macOS Fixes #1719 --- panda/src/glstuff/glGraphicsBuffer_src.cxx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/panda/src/glstuff/glGraphicsBuffer_src.cxx b/panda/src/glstuff/glGraphicsBuffer_src.cxx index 722e5091cf..3a02edf97c 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.cxx +++ b/panda/src/glstuff/glGraphicsBuffer_src.cxx @@ -523,6 +523,18 @@ rebuild_bitplanes() { // but it's a waste. Let's not do it unless the user requested stencil. _use_depth_stencil = false; +#ifdef __APPLE__ + } else if (_fb_properties.get_depth_bits() > 0 && _requested_multisamples) { + // Apple's OpenGL driver doesn't like blitting depth-stencil targets. + // See GitHub issue #1719 + _use_depth_stencil = false; + if (_fb_properties.get_depth_bits() < 24) { + // Make sure we do get at least as many depth bits as we would have + // gotten if we did get a depth-stencil buffer. + _fb_properties.set_depth_bits(24); + } +#endif + } else if (_fb_properties.get_depth_bits() > 0) { // Let's use a depth stencil buffer by default, if a depth buffer was // requested. From 287e456077701df8c3b06b9f71347d32f737c556 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Jan 2025 16:56:58 +0100 Subject: [PATCH 040/291] glgsg: Fix BGR swap regression for sync uploads Was regression in 2a47826101b4ad798564a38bc3b38109994e4444 --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 3a74f56331..08d4ad6ae5 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -14906,6 +14906,7 @@ upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload, external_format, num_components, component_width); ptimage = std::move(new_image); + image_ptr = ptimage; } } From 1fa8e2b0fc1d3926a1770e18d98f9c8f270a3a86 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Jan 2025 16:38:41 +0100 Subject: [PATCH 041/291] webgl: Don't use stringToUTF32 for keypress handling --- panda/src/webgldisplay/webGLGraphicsWindow.cxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/panda/src/webgldisplay/webGLGraphicsWindow.cxx b/panda/src/webgldisplay/webGLGraphicsWindow.cxx index 700f89c1b8..bfb359ac4e 100644 --- a/panda/src/webgldisplay/webGLGraphicsWindow.cxx +++ b/panda/src/webgldisplay/webGLGraphicsWindow.cxx @@ -472,9 +472,9 @@ on_keyboard_event(int type, const EmscriptenKeyboardEvent *event, void *user_dat // it does the right thing. We grab the first unicode code point. // Unfortunately, this doesn't seem to handle dead keys on Firefox. int keycode = 0; - EM_ASM_({ - stringToUTF32(String.fromCharCode($0), $1, 4); - }, event->charCode, &keycode); + keycode = EM_ASM_INT({ + return String.fromCharCode($0).codePointAt(0); + }, event->charCode); if (keycode != 0) { device->keystroke(keycode); From 1281875cbc871fd56abf1b3efe01c828f37bf5af Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Jan 2025 16:04:53 +0100 Subject: [PATCH 042/291] linmath: Fix degenerate case in decompose_matrix Regression in 583f7366db786ff750d77fb8ff09fd3c5718db1c --- panda/src/linmath/compose_matrix_src.cxx | 220 ++++++++++++----------- tests/linmath/test_compose_matrix.py | 17 ++ 2 files changed, 130 insertions(+), 107 deletions(-) diff --git a/panda/src/linmath/compose_matrix_src.cxx b/panda/src/linmath/compose_matrix_src.cxx index c3bb401c89..e5694b7119 100644 --- a/panda/src/linmath/compose_matrix_src.cxx +++ b/panda/src/linmath/compose_matrix_src.cxx @@ -323,79 +323,82 @@ unwind_yup_rotation(FLOATNAME(LMatrix3) &mat, FLOATNAME(LVecBase3) &hpr) { mat.get_row(z,2); // Project Z into the XZ plane. + FLOATTYPE heading = 0; FLOATNAME(LVector2) xz(z[0], z[2]); - xz = normalize(xz); + if (xz.normalize()) { + // Compute the rotation about the +Y (up) axis. This is yaw, or "heading". + heading = catan2(xz[0], xz[1]); - // Compute the rotation about the +Y (up) axis. This is yaw, or "heading". - FLOATTYPE heading = catan2(xz[0], xz[1]); + // Unwind the heading, and continue. + FLOATNAME(LMatrix3) rot_y; + rot_y._m(0, 0) = xz[1]; + rot_y._m(0, 1) = 0; + rot_y._m(0, 2) = xz[0]; - // Unwind the heading, and continue. - FLOATNAME(LMatrix3) rot_y; - rot_y._m(0, 0) = xz[1]; - rot_y._m(0, 1) = 0; - rot_y._m(0, 2) = xz[0]; + rot_y._m(1, 0) = 0; + rot_y._m(1, 1) = 1; + rot_y._m(1, 2) = 0; - rot_y._m(1, 0) = 0; - rot_y._m(1, 1) = 1; - rot_y._m(1, 2) = 0; + rot_y._m(2, 0) = -xz[0]; + rot_y._m(2, 1) = 0; + rot_y._m(2, 2) = xz[1]; - rot_y._m(2, 0) = -xz[0]; - rot_y._m(2, 1) = 0; - rot_y._m(2, 2) = xz[1]; - - x = x * rot_y; - y = y * rot_y; - z = z * rot_y; + x = x * rot_y; + y = y * rot_y; + z = z * rot_y; + } // Project the rotated Z into the YZ plane. + FLOATTYPE pitch = 0; FLOATNAME(LVector2) yz(z[1], z[2]); - yz = normalize(yz); + if (yz.normalize()) { + // Compute the rotation about the +X (right) axis. This is pitch. + pitch = -catan2(yz[0], yz[1]); - // Compute the rotation about the +X (right) axis. This is pitch. - FLOATTYPE pitch = -catan2(yz[0], yz[1]); + // Unwind the pitch. + FLOATNAME(LMatrix3) rot_x; + rot_x._m(0, 0) = 1; + rot_x._m(0, 1) = 0; + rot_x._m(0, 2) = 0; - // Unwind the pitch. - FLOATNAME(LMatrix3) rot_x; - rot_x._m(0, 0) = 1; - rot_x._m(0, 1) = 0; - rot_x._m(0, 2) = 0; + rot_x._m(1, 0) = 0; + rot_x._m(1, 1) = yz[1]; + rot_x._m(1, 2) = yz[0]; - rot_x._m(1, 0) = 0; - rot_x._m(1, 1) = yz[1]; - rot_x._m(1, 2) = yz[0]; + rot_x._m(2, 0) = 0; + rot_x._m(2, 1) = -yz[0]; + rot_x._m(2, 2) = yz[1]; - rot_x._m(2, 0) = 0; - rot_x._m(2, 1) = -yz[0]; - rot_x._m(2, 2) = yz[1]; - - x = x * rot_x; - y = y * rot_x; - z = z * rot_x; + x = x * rot_x; + y = y * rot_x; + z = z * rot_x; + } // Project the rotated X onto the XY plane. + FLOATTYPE roll = 0; FLOATNAME(LVector2) xy(x[0], x[1]); - xy = normalize(xy); + if (xy.normalize()) { + // Compute the rotation about the +Z (back) axis. This is roll. + roll = -catan2(xy[1], xy[0]); - // Compute the rotation about the +Z (back) axis. This is roll. - FLOATTYPE roll = -catan2(xy[1], xy[0]); + // Unwind the roll from the axes, and continue. + FLOATNAME(LMatrix3) rot_z; + rot_z._m(0, 0) = xy[0]; + rot_z._m(0, 1) = -xy[1]; + rot_z._m(0, 2) = 0; - // Unwind the roll from the axes, and continue. - FLOATNAME(LMatrix3) rot_z; - rot_z._m(0, 0) = xy[0]; - rot_z._m(0, 1) = -xy[1]; - rot_z._m(0, 2) = 0; + rot_z._m(1, 0) = xy[1]; + rot_z._m(1, 1) = xy[0]; + rot_z._m(1, 2) = 0; - rot_z._m(1, 0) = xy[1]; - rot_z._m(1, 1) = xy[0]; - rot_z._m(1, 2) = 0; + rot_z._m(2, 0) = 0; + rot_z._m(2, 1) = 0; + rot_z._m(2, 2) = 1; - rot_z._m(2, 0) = 0; - rot_z._m(2, 1) = 0; - rot_z._m(2, 2) = 1; - - x = x * rot_z; - y = y * rot_z; - z = z * rot_z; + x = x * rot_z; + y = y * rot_z; + z = z * rot_z; + } // Reset the matrix to reflect the unwinding. mat.set_row(0, x); @@ -425,79 +428,82 @@ unwind_zup_rotation(FLOATNAME(LMatrix3) &mat, FLOATNAME(LVecBase3) &hpr) { mat.get_row(z,2); // Project Y into the XY plane. + FLOATTYPE heading = 0; FLOATNAME(LVector2) xy(y[0], y[1]); - xy = normalize(xy); + if (xy.normalize()) { + // Compute the rotation about the +Z (up) axis. This is yaw, or "heading". + heading = -catan2(xy[0], xy[1]); - // Compute the rotation about the +Z (up) axis. This is yaw, or "heading". - FLOATTYPE heading = -catan2(xy[0], xy[1]); + // Unwind the heading, and continue. + FLOATNAME(LMatrix3) rot_z; + rot_z._m(0, 0) = xy[1]; + rot_z._m(0, 1) = xy[0]; + rot_z._m(0, 2) = 0; - // Unwind the heading, and continue. - FLOATNAME(LMatrix3) rot_z; - rot_z._m(0, 0) = xy[1]; - rot_z._m(0, 1) = xy[0]; - rot_z._m(0, 2) = 0; + rot_z._m(1, 0) = -xy[0]; + rot_z._m(1, 1) = xy[1]; + rot_z._m(1, 2) = 0; - rot_z._m(1, 0) = -xy[0]; - rot_z._m(1, 1) = xy[1]; - rot_z._m(1, 2) = 0; + rot_z._m(2, 0) = 0; + rot_z._m(2, 1) = 0; + rot_z._m(2, 2) = 1; - rot_z._m(2, 0) = 0; - rot_z._m(2, 1) = 0; - rot_z._m(2, 2) = 1; - - x = x * rot_z; - y = y * rot_z; - z = z * rot_z; + x = x * rot_z; + y = y * rot_z; + z = z * rot_z; + } // Project the rotated Y into the YZ plane. + FLOATTYPE pitch = 0; FLOATNAME(LVector2) yz(y[1], y[2]); - yz = normalize(yz); + if (yz.normalize()) { + // Compute the rotation about the +X (right) axis. This is pitch. + pitch = catan2(yz[1], yz[0]); - // Compute the rotation about the +X (right) axis. This is pitch. - FLOATTYPE pitch = catan2(yz[1], yz[0]); + // Unwind the pitch. + FLOATNAME(LMatrix3) rot_x; + rot_x._m(0, 0) = 1; + rot_x._m(0, 1) = 0; + rot_x._m(0, 2) = 0; - // Unwind the pitch. - FLOATNAME(LMatrix3) rot_x; - rot_x._m(0, 0) = 1; - rot_x._m(0, 1) = 0; - rot_x._m(0, 2) = 0; + rot_x._m(1, 0) = 0; + rot_x._m(1, 1) = yz[0]; + rot_x._m(1, 2) = -yz[1]; - rot_x._m(1, 0) = 0; - rot_x._m(1, 1) = yz[0]; - rot_x._m(1, 2) = -yz[1]; + rot_x._m(2, 0) = 0; + rot_x._m(2, 1) = yz[1]; + rot_x._m(2, 2) = yz[0]; - rot_x._m(2, 0) = 0; - rot_x._m(2, 1) = yz[1]; - rot_x._m(2, 2) = yz[0]; - - x = x * rot_x; - y = y * rot_x; - z = z * rot_x; + x = x * rot_x; + y = y * rot_x; + z = z * rot_x; + } // Project X into the XZ plane. + FLOATTYPE roll = 0; FLOATNAME(LVector2) xz(x[0], x[2]); - xz = normalize(xz); - + if (xz.normalize()) { // Compute the rotation about the -Y (back) axis. This is roll. - FLOATTYPE roll = -catan2(xz[1], xz[0]); + roll = -catan2(xz[1], xz[0]); - // Unwind the roll from the axes, and continue. - FLOATNAME(LMatrix3) rot_y; - rot_y._m(0, 0) = xz[0]; - rot_y._m(0, 1) = 0; - rot_y._m(0, 2) = -xz[1]; + // Unwind the roll from the axes, and continue. + FLOATNAME(LMatrix3) rot_y; + rot_y._m(0, 0) = xz[0]; + rot_y._m(0, 1) = 0; + rot_y._m(0, 2) = -xz[1]; - rot_y._m(1, 0) = 0; - rot_y._m(1, 1) = 1; - rot_y._m(1, 2) = 0; + rot_y._m(1, 0) = 0; + rot_y._m(1, 1) = 1; + rot_y._m(1, 2) = 0; - rot_y._m(2, 0) = xz[1]; - rot_y._m(2, 1) = 0; - rot_y._m(2, 2) = xz[0]; + rot_y._m(2, 0) = xz[1]; + rot_y._m(2, 1) = 0; + rot_y._m(2, 2) = xz[0]; - x = x * rot_y; - y = y * rot_y; - z = z * rot_y; + x = x * rot_y; + y = y * rot_y; + z = z * rot_y; + } // Reset the matrix to reflect the unwinding. mat.set_row(0, x); diff --git a/tests/linmath/test_compose_matrix.py b/tests/linmath/test_compose_matrix.py index 82497cb67d..bd52875a02 100644 --- a/tests/linmath/test_compose_matrix.py +++ b/tests/linmath/test_compose_matrix.py @@ -24,3 +24,20 @@ def test_compose_matrix(coordsys): new_quat = core.LQuaternion() new_quat.set_hpr(new_hpr, coordsys) assert quat.is_same_direction(new_quat) + + +@pytest.mark.parametrize("coordsys", (core.CS_zup_right, core.CS_yup_right, core.CS_zup_left, core.CS_yup_left)) +def test_compose_matrix2(coordsys): + mat = core.LMatrix3(1, 0, 0, 0, 0, -1, 0, 1, 0) + + new_scale = core.LVecBase3() + new_hpr = core.LVecBase3() + new_shear = core.LVecBase3() + core.decompose_matrix(mat, new_scale, new_shear, new_hpr, coordsys) + + assert new_scale.almost_equal(core.LVecBase3(1, 1, 1)) + if coordsys in (core.CS_zup_left, core.CS_yup_left): + assert new_hpr.almost_equal(core.LVecBase3(0, 90, 0)) + else: + assert new_hpr.almost_equal(core.LVecBase3(0, -90, 0)) + assert new_shear.almost_equal(core.LVecBase3(0, 0, 0)) From 8114971c56ce89781e58336be6d5b349aca07b5a Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Jan 2025 16:57:54 +0100 Subject: [PATCH 043/291] webgl: Improve handling of canvas focus Can now forcibly focus to the canvas with a foreground request, and now focuses automatically on window open (except if requested not to) --- .../src/webgldisplay/webGLGraphicsWindow.cxx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/panda/src/webgldisplay/webGLGraphicsWindow.cxx b/panda/src/webgldisplay/webGLGraphicsWindow.cxx index bfb359ac4e..dc6d8e6f97 100644 --- a/panda/src/webgldisplay/webGLGraphicsWindow.cxx +++ b/panda/src/webgldisplay/webGLGraphicsWindow.cxx @@ -238,6 +238,16 @@ set_properties_now(WindowProperties &properties) { // though, we can't hide the cursor. properties.clear_cursor_hidden(); } + + if (properties.get_foreground()) { + EM_ASM_({ + var canvas = document.getElementById('canvas'); + if (canvas) { + canvas.focus(); + } + }); + properties.clear_foreground(); + } } /** @@ -369,6 +379,19 @@ open_window() { emscripten_set_wheel_callback(target, user_data, false, &on_wheel_event); + if (!_properties.has_foreground() || _properties.get_foreground()) { + _properties.set_foreground(EM_ASM_INT({ + var canvas = document.getElementById('canvas'); + if (canvas) { + canvas.focus(); + + return document.activeElement === canvas; + } else { + return false; + } + })); + } + return true; } From f6f391bd3decd945daafba06eda98999c6809856 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 30 Jan 2025 11:18:28 +0100 Subject: [PATCH 044/291] glgsg: Don't override texture clear color for FBO attachments This applies to FBOs without a clear set; the clear color set on the texture should determine the initial color. --- panda/src/glstuff/glGraphicsBuffer_src.cxx | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/panda/src/glstuff/glGraphicsBuffer_src.cxx b/panda/src/glstuff/glGraphicsBuffer_src.cxx index 3a02edf97c..9a38590b8b 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.cxx +++ b/panda/src/glstuff/glGraphicsBuffer_src.cxx @@ -640,10 +640,23 @@ rebuild_bitplanes() { if (_have_any_color || have_any_depth) { // Clear if the fbo was just created, regardless of the clear settings per - // frame. + // frame. However, we don't do this for textures, which may have useful + // contents that need to be preserved. if (_initial_clear) { - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + GLbitfield mask = 0; + if (_rb[RTP_color]) { + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + mask |= GL_COLOR_BUFFER_BIT; + } + if (_rb[RTP_depth]) { + mask |= GL_DEPTH_BUFFER_BIT; + } + if (_rb[RTP_depth_stencil]) { + mask |= GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; + } + if (mask != 0) { + glClear(mask); + } } #ifndef OPENGLES_1 } else if (glgsg->_supports_empty_framebuffer) { @@ -808,6 +821,10 @@ bind_slot(int layer, bool rb_resize, Texture **attach, RenderTexturePlane slot, _fb_properties.setup_color_texture(tex); } + if (slot == RTP_color && !tex->has_clear_color()) { + tex->set_clear_color(LColor(0, 0, 0, 1)); + } + _textures.push_back(tex); TextureContext *tc = tex->prepare_now(glgsg->get_prepared_objects(), glgsg); From 99b388871219be723ef2cad91c7606875d1e471e Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 30 Jan 2025 11:38:00 +0100 Subject: [PATCH 045/291] gobj: Fix Texture::clear_clear_color() --- panda/src/gobj/texture.I | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/gobj/texture.I b/panda/src/gobj/texture.I index d304bfbfed..de82e7580c 100644 --- a/panda/src/gobj/texture.I +++ b/panda/src/gobj/texture.I @@ -278,7 +278,7 @@ set_clear_color(const LColor &color) { INLINE void Texture:: clear_clear_color() { CDWriter cdata(_cycler, true); - cdata->_has_clear_color = true; + cdata->_has_clear_color = false; } /** From f188c8a028e5222a2f1d44c49d48cdc7e4def18c Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 30 Jan 2025 11:38:17 +0100 Subject: [PATCH 046/291] tests: Use approx comparison for test_depth_bias --- tests/display/test_depth_buffer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/display/test_depth_buffer.py b/tests/display/test_depth_buffer.py index cdbaffd370..c33ddbb6f1 100644 --- a/tests/display/test_depth_buffer.py +++ b/tests/display/test_depth_buffer.py @@ -198,12 +198,12 @@ def test_depth_bias(depth_region): # With slope-scaled depth bias (our quad has no slope) state = core.RenderState.make(core.DepthBiasAttrib.make(10, 0)) z = render_depth_pixel(depth_region, 5, near=1, far=10, state=state) - assert z == z_ref + assert z == pytest.approx(z_ref) # Same, but negative state = core.RenderState.make(core.DepthBiasAttrib.make(-10, 0)) z = render_depth_pixel(depth_region, 5, near=1, far=10, state=state) - assert z == z_ref + assert z == pytest.approx(z_ref) def test_depth_offset(depth_region): From e681c17215c8b95c0b4f461b8b09569a38716bd4 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 30 Jan 2025 11:58:25 +0100 Subject: [PATCH 047/291] glgsg: Use glClearTexImage even when tex has no storage Just use glTexImage with null image to create the storage first, then clear it, this prevents needlessly generating a filled RAM image on the CPU --- .../glstuff/glGraphicsStateGuardian_src.cxx | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 08d4ad6ae5..8bea9aa06b 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -14675,6 +14675,11 @@ upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload, << "loading new texture object for " << tex->get_name() << " view " << view << ", " << width << " x " << height << " x " << depth << ", mipmaps " << num_ram_mipmap_levels << " / " << num_levels; + + if (num_ram_mipmap_levels == 0 && tex->has_clear_color()) { + GLCAT.debug(false) + << ", clearing to " << tex->get_clear_color(); + } } else { // Try to subload the image over the existing GL Texture object, possibly @@ -14758,28 +14763,36 @@ upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload, // level to a solid color. #ifndef OPENGLES if (target != GL_TEXTURE_BUFFER) { - if (_supports_clear_texture && !needs_reload) { - // We can do that with the convenient glClearTexImage - // function. - vector_uchar clear_data = tex->get_clear_data(); + if (_supports_clear_texture && !compressed) { + // If we don't have storage, we create it with a NULL image first. + if (!needs_reload || + upload_texture_level(true, false, target, level, + width, height, depth, internal_format, + external_format, component_type, + nullptr, 0, pages, usage)) { + vector_uchar clear_data = tex->get_clear_data(); - if (pages.has_all_of(0, depth)) { - _glClearTexImage(index, level, external_format, - component_type, (void *)&clear_data[0]); + if (needs_reload || pages.has_all_of(0, depth)) { + _glClearTexImage(index, level, external_format, + component_type, (void *)&clear_data[0]); + } + else for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) { + int begin = pages.get_subrange_begin(sri); + int num_pages = pages.get_subrange_end(sri) - begin; + _glClearTexSubImage(index, level, 0, 0, begin, + width, height, num_pages, external_format, + component_type, (void *)&clear_data[0]); + } + continue; } - else for (size_t sri = 0; sri < pages.get_num_subranges(); ++sri) { - int begin = pages.get_subrange_begin(sri); - int num_pages = pages.get_subrange_end(sri) - begin; - _glClearTexSubImage(index, level, 0, 0, begin, - width, height, num_pages, external_format, - component_type, (void *)&clear_data[0]); - } - continue; } } else { - if (_supports_clear_buffer && !needs_reload) { + if (_supports_clear_buffer) { // For buffer textures we need to clear the underlying // storage. + if (needs_reload) { + _glBufferData(GL_TEXTURE_BUFFER, tex->get_expected_ram_mipmap_view_size(n), nullptr, usage); + } vector_uchar clear_data = tex->get_clear_data(); _glClearBufferData(GL_TEXTURE_BUFFER, internal_format, external_format, From 0404febf96645f9ab3f913717263735f94a27d82 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 30 Jan 2025 14:13:15 +0100 Subject: [PATCH 048/291] windisplay: Fix IME result not showing up without `ime-aware true` --- panda/src/windisplay/winGraphicsWindow.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/windisplay/winGraphicsWindow.cxx b/panda/src/windisplay/winGraphicsWindow.cxx index 3c769434a5..266722aa1b 100644 --- a/panda/src/windisplay/winGraphicsWindow.cxx +++ b/panda/src/windisplay/winGraphicsWindow.cxx @@ -2064,7 +2064,7 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { // the actual value passed to WM_CHAR seems to be poorly defined. Now we // are using RegisterClassW etc., which means WM_CHAR is absolutely // supposed to be utf-16. - if (!_ime_open) { + if (!_ime_open || !ime_aware) { _input->keystroke(wparam); } break; From b0e1e0c75a9af617060e224b5472cb68e9a7c13c Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Sun, 12 Jan 2025 17:47:15 -0700 Subject: [PATCH 049/291] showbase: Annotate callback methods --- direct/src/showbase/ShowBase.py | 42 ++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 40ebc8b8b9..26370c65a1 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -177,6 +177,8 @@ class ShowBase(DirectObject.DirectObject): aspect2d: NodePath pixel2d: NodePath + cluster: Any | None + def __init__(self, fStartDirect: bool = True, windowType: str | None = None) -> None: """Opens a window, sets up a 3-D and several 2-D scene graphs, and everything else needed to render the scene graph to the window. @@ -1208,7 +1210,7 @@ class ShowBase(DirectObject.DirectObject): self.taskMgr.remove('clientSleep') self.taskMgr.add(self.__sleepCycleTask, 'clientSleep', sort = 55) - def __sleepCycleTask(self, task): + def __sleepCycleTask(self, task: object) -> int: Thread.sleep(self.clientSleep) #time.sleep(self.clientSleep) return Task.cont @@ -1444,7 +1446,7 @@ class ShowBase(DirectObject.DirectObject): self.__configAspectRatio = aspectRatio self.adjustWindowAspectRatio(self.getAspectRatio()) - def getAspectRatio(self, win = None): + def getAspectRatio(self, win: GraphicsOutput | None = None) -> float: # Returns the actual aspect ratio of the indicated (or main # window), or the default aspect ratio if there is not yet a # main window. @@ -1453,7 +1455,7 @@ class ShowBase(DirectObject.DirectObject): if self.__configAspectRatio: return self.__configAspectRatio - aspectRatio = 1 + aspectRatio: float = 1 if win is None: win = self.win @@ -1476,7 +1478,7 @@ class ShowBase(DirectObject.DirectObject): return aspectRatio - def getSize(self, win = None): + def getSize(self, win: GraphicsOutput | None = None) -> tuple[int, int]: """ Returns the actual size of the indicated (or main window), or the default size if there is not yet a main window. @@ -2176,7 +2178,7 @@ class ShowBase(DirectObject.DirectObject): music.setLoop(looping) music.play() - def __resetPrevTransform(self, state): + def __resetPrevTransform(self, state: object) -> int: # Clear out the previous velocity deltas now, after we have # rendered (the previous frame). We do this after the render, # so that we have a chance to draw a representation of spheres @@ -2187,7 +2189,7 @@ class ShowBase(DirectObject.DirectObject): PandaNode.resetAllPrevTransform() return Task.cont - def __dataLoop(self, state): + def __dataLoop(self, state: object) -> int: # Check if there were newly connected devices. self.devices.update() @@ -2197,7 +2199,7 @@ class ShowBase(DirectObject.DirectObject): self.dgTrav.traverse(self.dataRootNode) return Task.cont - def __ivalLoop(self, state): + def __ivalLoop(self, state: object) -> int: # Execute all intervals in the global ivalMgr. IntervalManager.ivalMgr.step() return Task.cont @@ -2215,7 +2217,7 @@ class ShowBase(DirectObject.DirectObject): self.shadowTrav.traverse(self.render) return Task.cont - def __collisionLoop(self, state): + def __collisionLoop(self, state: object) -> int: # run the collision traversal if we have a # CollisionTraverser set. if self.cTrav: @@ -2227,14 +2229,14 @@ class ShowBase(DirectObject.DirectObject): messenger.send("collisionLoopFinished") return Task.cont - def __audioLoop(self, state): + def __audioLoop(self, state: object) -> int: if self.musicManager is not None: self.musicManager.update() for x in self.sfxManagerList: x.update() return Task.cont - def __garbageCollectStates(self, state): + def __garbageCollectStates(self, state: object) -> int: """ This task is started only when we have garbage-collect-states set in the Config.prc file, in which case we're responsible for taking out Panda's garbage from @@ -2245,7 +2247,7 @@ class ShowBase(DirectObject.DirectObject): RenderState.garbageCollect() return Task.cont - def __igLoop(self, state): + def __igLoop(self, state: object) -> int: if __debug__: # We render the watch variables for the onScreenDebug as soon # as we reasonably can before the renderFrame(). @@ -2285,7 +2287,7 @@ class ShowBase(DirectObject.DirectObject): throw_new_frame() return Task.cont - def __igLoopSync(self, state): + def __igLoopSync(self, state: object) -> int: if __debug__: # We render the watch variables for the onScreenDebug as soon # as we reasonably can before the renderFrame(). @@ -2294,6 +2296,7 @@ class ShowBase(DirectObject.DirectObject): if self.recorder: self.recorder.recordFrame() + assert self.cluster is not None self.cluster.collectData() # Finally, render the frame. @@ -2323,6 +2326,7 @@ class ShowBase(DirectObject.DirectObject): time.sleep(0.1) self.graphicsEngine.readyFlip() + assert self.cluster is not None self.cluster.waitForFlipCommand() self.graphicsEngine.flipFrame() @@ -2753,7 +2757,7 @@ class ShowBase(DirectObject.DirectObject): self.oobeVis.reparentTo(self.camera) self.oobeMode = 1 - def __oobeButton(self, suffix, button): + def __oobeButton(self, suffix: str, button: str) -> None: if button.startswith('mouse'): # Eat mouse buttons. return @@ -3073,7 +3077,7 @@ class ShowBase(DirectObject.DirectObject): else: return Task.cont - def windowEvent(self, win): + def windowEvent(self, win: GraphicsOutput) -> None: if win != self.win: # This event isn't about our window. return @@ -3092,9 +3096,9 @@ class ShowBase(DirectObject.DirectObject): self.userExit() if properties.getForeground() and not self.mainWinForeground: - self.mainWinForeground = 1 + self.mainWinForeground = True elif not properties.getForeground() and self.mainWinForeground: - self.mainWinForeground = 0 + self.mainWinForeground = False if __debug__: if self.__autoGarbageLogging: GarbageReport.b_checkForGarbageLeaks() @@ -3102,12 +3106,12 @@ class ShowBase(DirectObject.DirectObject): if properties.getMinimized() and not self.mainWinMinimized: # If the main window is minimized, throw an event to # stop the music. - self.mainWinMinimized = 1 + self.mainWinMinimized = True messenger.send('PandaPaused') elif not properties.getMinimized() and self.mainWinMinimized: # If the main window is restored, throw an event to # restart the music. - self.mainWinMinimized = 0 + self.mainWinMinimized = False messenger.send('PandaRestarted') # If we have not forced the aspect ratio, let's see if it has @@ -3125,7 +3129,7 @@ class ShowBase(DirectObject.DirectObject): if self.wantRender2dp: self.pixel2dp.setScale(2.0 / xsize, 1.0, 2.0 / ysize) - def adjustWindowAspectRatio(self, aspectRatio): + def adjustWindowAspectRatio(self, aspectRatio: float) -> None: """ This function is normally called internally by `windowEvent()`, but it may also be called to explicitly adjust the aspect ratio of the render/render2d DisplayRegion, by a From 3f0f90d83db21ab718e624bcbd28b042ab74435c Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 2 Feb 2025 20:06:46 +0100 Subject: [PATCH 050/291] makepanda: Don't try to call interrogate_module with -vv --- makepanda/makepandacore.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 235385f857..49d2034dbd 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -666,11 +666,12 @@ def oscmd(cmd, ignoreError = False, cwd=None): print(GetColor("blue") + cmd.split(" ", 1)[0] + " " + GetColor("magenta") + cmd.split(" ", 1)[1] + GetColor()) sys.stdout.flush() + if cmd[0] == '"': + exe = cmd[1 : cmd.index('"', 1)] + else: + exe = cmd.split()[0] + if sys.platform == "win32": - if cmd[0] == '"': - exe = cmd[1 : cmd.index('"', 1)] - else: - exe = cmd.split()[0] exe_path = LocateBinary(exe) if exe_path is None: exit("Cannot find "+exe+" on search path") @@ -706,7 +707,7 @@ def oscmd(cmd, ignoreError = False, cwd=None): exit("") if res != 0 and not ignoreError: - if "interrogate" in cmd.split(" ", 1)[0] and GetVerbose(): + if "interrogate" in exe and "interrogate_module" not in exe and GetVerbose(): print(ColorText("red", "Interrogate failed, retrieving debug output...")) sys.stdout.flush() verbose_cmd = cmd.split(" ", 1)[0] + " -vv " + cmd.split(" ", 1)[1] From ea1cbe47ac8f476e0d9ebd3e77f30c1d56b33fcf Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 2 Feb 2025 20:07:10 +0100 Subject: [PATCH 051/291] dist: Add missing hidden import for panda3d.core on enum --- direct/src/dist/FreezeTool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index aad6dc4b24..37b697f878 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -88,6 +88,7 @@ defaultHiddenImports = { 'numpy.core._dtype_ctypes', 'numpy.core._methods', ], + 'panda3d.core': ['enum'], 'pandas.compat': ['lzma', 'cmath'], 'pandas._libs.tslibs.conversion': ['pandas._libs.tslibs.base'], 'plyer': ['plyer.platforms'], From 79d6727077c78e5f2dbb7c9dd85ccbc81ad40b6c Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 2 Feb 2025 20:11:32 +0100 Subject: [PATCH 052/291] Move MAIN_DIR initialization code from interrogate to panda3d Using the new -init flag, see panda3d/interrogate#3 --- cmake/macros/Interrogate.cmake | 8 +- dtool/Config.cmake | 2 +- dtool/src/dtoolutil/CMakeLists.txt | 2 + .../dtoolutil/p3dtoolutil_ext_composite.cxx | 1 + dtool/src/dtoolutil/pyenv_init.cxx | 80 +++++++++++++++++++ dtool/src/dtoolutil/pyenv_init.h | 14 ++++ makepanda/makepanda.py | 5 +- makepanda/makepandacore.py | 4 +- panda/CMakeLists.txt | 2 +- 9 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 dtool/src/dtoolutil/pyenv_init.cxx create mode 100644 dtool/src/dtoolutil/pyenv_init.h diff --git a/cmake/macros/Interrogate.cmake b/cmake/macros/Interrogate.cmake index 5f26cebbe9..7533be7564 100644 --- a/cmake/macros/Interrogate.cmake +++ b/cmake/macros/Interrogate.cmake @@ -283,7 +283,7 @@ endfunction(interrogate_sources) # # Function: add_python_module(module [lib1 [lib2 ...]] [LINK lib1 ...] -# [IMPORT mod1 ...]) +# [IMPORT mod1 ...] [INIT func1 ...]) # Uses interrogate to create a Python module. If the LINK keyword is specified, # the Python module is linked against the specified libraries instead of those # listed before. The IMPORT keyword makes the output module import another @@ -305,7 +305,7 @@ function(add_python_module module) set(keyword) foreach(arg ${ARGN}) - if(arg STREQUAL "LINK" OR arg STREQUAL "IMPORT" OR arg STREQUAL "COMPONENT") + if(arg STREQUAL "LINK" OR arg STREQUAL "IMPORT" OR arg STREQUAL "INIT" OR arg STREQUAL "COMPONENT") set(keyword "${arg}") elseif(keyword STREQUAL "LINK") @@ -316,6 +316,10 @@ function(add_python_module module) list(APPEND import_flags "-import" "${arg}") set(keyword) + elseif(keyword STREQUAL "INIT") + list(APPEND import_flags "-init" "${arg}") + set(keyword) + elseif(keyword STREQUAL "COMPONENT") set(component "${arg}") set(keyword) diff --git a/dtool/Config.cmake b/dtool/Config.cmake index f3063d86ad..67ea64c9ce 100644 --- a/dtool/Config.cmake +++ b/dtool/Config.cmake @@ -265,7 +265,7 @@ if(BUILD_INTERROGATE) panda3d-interrogate GIT_REPOSITORY https://github.com/panda3d/interrogate.git - GIT_TAG 03418d6d7ddda7fb99abf27230aa42d1d8bd607e + GIT_TAG d2844d994fcc465a4e22b10001d3ac5c4012b814 PREFIX ${_interrogate_dir} CMAKE_ARGS diff --git a/dtool/src/dtoolutil/CMakeLists.txt b/dtool/src/dtoolutil/CMakeLists.txt index 5885534178..fdab210e14 100644 --- a/dtool/src/dtoolutil/CMakeLists.txt +++ b/dtool/src/dtoolutil/CMakeLists.txt @@ -69,6 +69,8 @@ set(P3DTOOLUTIL_IGATEEXT globPattern_ext.h iostream_ext.cxx iostream_ext.h + pyenv_init.cxx + pyenv_init.h textEncoder_ext.cxx textEncoder_ext.h ) diff --git a/dtool/src/dtoolutil/p3dtoolutil_ext_composite.cxx b/dtool/src/dtoolutil/p3dtoolutil_ext_composite.cxx index ceb2f01bf1..3a8de69d4d 100644 --- a/dtool/src/dtoolutil/p3dtoolutil_ext_composite.cxx +++ b/dtool/src/dtoolutil/p3dtoolutil_ext_composite.cxx @@ -1,4 +1,5 @@ #include "filename_ext.cxx" #include "globPattern_ext.cxx" #include "iostream_ext.cxx" +#include "pyenv_init.cxx" #include "textEncoder_ext.cxx" diff --git a/dtool/src/dtoolutil/pyenv_init.cxx b/dtool/src/dtoolutil/pyenv_init.cxx new file mode 100644 index 0000000000..d806a8ef7c --- /dev/null +++ b/dtool/src/dtoolutil/pyenv_init.cxx @@ -0,0 +1,80 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file pyenv_init.cxx + * @author rdb + * @date 2025-02-02 + */ + +#include "pyenv_init.h" +#include "py_panda.h" +#include "executionEnvironment.h" + +/** + * Called when panda3d.core is initialized, does some initialization specific + * to the Python environment. + */ +void +pyenv_init() { + // MAIN_DIR needs to be set very early; this seems like a convenient place + // to do that. Perhaps we'll find a better place for this in the future. + static bool initialized_main_dir = false; + if (!initialized_main_dir) { + /*if (interrogatedb_cat.is_debug()) { + // Good opportunity to print this out once, at startup. + interrogatedb_cat.debug() + << "Python " << version << "\n"; + }*/ + + if (!ExecutionEnvironment::has_environment_variable("MAIN_DIR")) { + // Grab the __main__ module and extract its __file__ attribute. + Filename main_dir; + PyObject *main_module = PyImport_ImportModule("__main__"); + PyObject *file_attr = nullptr; + if (main_module != nullptr) { + file_attr = PyObject_GetAttrString(main_module, "__file__"); + } else { + std::cerr << "Warning: unable to import __main__\n"; + } + if (file_attr == nullptr) { + // Must be running in the interactive interpreter. Use the CWD. + main_dir = ExecutionEnvironment::get_cwd(); + } else { +#if PY_MAJOR_VERSION >= 3 + Py_ssize_t length; + wchar_t *buffer = PyUnicode_AsWideCharString(file_attr, &length); + if (buffer != nullptr) { + main_dir = Filename::from_os_specific_w(std::wstring(buffer, length)); + main_dir.make_absolute(); + main_dir = main_dir.get_dirname(); + PyMem_Free(buffer); + } +#else + char *buffer; + Py_ssize_t length; + if (PyString_AsStringAndSize(file_attr, &buffer, &length) != -1) { + main_dir = Filename::from_os_specific(std::string(buffer, length)); + main_dir.make_absolute(); + main_dir = main_dir.get_dirname(); + } +#endif + else { + std::cerr << "Invalid string for __main__.__file__\n"; + } + } + ExecutionEnvironment::shadow_environment_variable("MAIN_DIR", main_dir.to_os_specific()); + PyErr_Clear(); + } + initialized_main_dir = true; + } + + // Also, while we are at it, initialize the thread swap hook. +#if defined(HAVE_THREADS) && defined(SIMPLE_THREADS) + global_thread_state_swap = PyThreadState_Swap; +#endif +} diff --git a/dtool/src/dtoolutil/pyenv_init.h b/dtool/src/dtoolutil/pyenv_init.h new file mode 100644 index 0000000000..2369ca1871 --- /dev/null +++ b/dtool/src/dtoolutil/pyenv_init.h @@ -0,0 +1,14 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file pyenv_init.h + * @author rdb + * @date 2025-02-02 + */ + +extern "C" void pyenv_init(); diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index f04bb08f1c..3e27dd19fb 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1651,6 +1651,9 @@ def CompileImod(wobj, wsrc, opts): importmod = GetValueOption(opts, "IMPORT:") if importmod: cmd += ' -import ' + importmod + initfunc = GetValueOption(opts, "INIT:") + if initfunc: + cmd += ' -init ' + initfunc for x in wsrc: cmd += ' ' + BracketNameWithQuotes(x) oscmd(cmd) CompileCxx(wobj,woutc,opts) @@ -4168,7 +4171,7 @@ if GetTarget() != "emscripten": if PkgSkip("FREETYPE")==0: PyTargetAdd('core_module.obj', input='libp3pnmtext.in') -PyTargetAdd('core_module.obj', opts=['IMOD:panda3d.core', 'ILIB:core']) +PyTargetAdd('core_module.obj', opts=['IMOD:panda3d.core', 'ILIB:core', 'INIT:pyenv_init']) PyTargetAdd('core.pyd', input='libp3dtoolbase_igate.obj') PyTargetAdd('core.pyd', input='p3dtoolbase_typeHandle_ext.obj') diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 49d2034dbd..c3c101486d 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -589,8 +589,8 @@ def GetInterrogateDir(): return INTERROGATE_DIR dir = os.path.join(GetOutputDir(), "tmp", "interrogate") - if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.4.0.dist-info")): - oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.4.0" % (sys.executable, dir)) + if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.5.0.dist-info")): + oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.5.0" % (sys.executable, dir)) INTERROGATE_DIR = dir diff --git a/panda/CMakeLists.txt b/panda/CMakeLists.txt index cb33932d9c..e118a8a5b9 100644 --- a/panda/CMakeLists.txt +++ b/panda/CMakeLists.txt @@ -107,7 +107,7 @@ if(HAVE_FREETYPE) endif() if(INTERROGATE_PYTHON_INTERFACE) - add_python_module(panda3d.core ${CORE_MODULE_COMPONENTS} LINK panda) + add_python_module(panda3d.core ${CORE_MODULE_COMPONENTS} LINK panda INIT pyenv_init) # Generate our __init__.py if(WIN32) From 8a1159f4a7b9dc44c7519524ac09136777e24236 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 2 Feb 2025 20:12:17 +0100 Subject: [PATCH 053/291] makepanda: Only build pnmimagetypes with exceptions if using OpenEXR --- makepanda/makepanda.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 3e27dd19fb..7e4588051a 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -4027,7 +4027,9 @@ TargetAdd('libp3pgui.in', opts=['IMOD:panda3d.core', 'ILIB:libp3pgui', 'SRCDIR:p # DIRECTORY: panda/src/pnmimagetypes/ # -OPTS=['DIR:panda/src/pnmimagetypes', 'DIR:panda/src/pnmimage', 'BUILDING:PANDA', 'PNG', 'ZLIB', 'JPEG', 'TIFF', 'OPENEXR', 'EXCEPTIONS'] +OPTS=['DIR:panda/src/pnmimagetypes', 'DIR:panda/src/pnmimage', 'BUILDING:PANDA', 'PNG', 'ZLIB', 'JPEG', 'TIFF', 'OPENEXR'] +if not PkgSkip('OPENEXR') and GetTarget() != 'emscripten': + OPTS.append('EXCEPTIONS') TargetAdd('p3pnmimagetypes_composite1.obj', opts=OPTS, input='p3pnmimagetypes_composite1.cxx') TargetAdd('p3pnmimagetypes_composite2.obj', opts=OPTS, input='p3pnmimagetypes_composite2.cxx') From 6452907b64daa1fd810a5d80bb38563e33e8703d Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 3 Feb 2025 18:06:30 +0100 Subject: [PATCH 054/291] Improvements for env handling with node.js and emscripten - Initializes the environment so that getenv et al works properly - No longer calls JS functions at static init time; the JS preamble will pass the right env vars to ExecutionEnvironment - Sets the binary name to the path of the .js file --- dtool/src/dtoolutil/console_preamble.js | 50 ++++++++++++++++++++ dtool/src/dtoolutil/executionEnvironment.cxx | 25 +++++----- dtool/src/dtoolutil/executionEnvironment.h | 8 ++-- makepanda/makepanda.py | 5 +- tests/main.c | 42 ---------------- 5 files changed, 70 insertions(+), 60 deletions(-) create mode 100644 dtool/src/dtoolutil/console_preamble.js diff --git a/dtool/src/dtoolutil/console_preamble.js b/dtool/src/dtoolutil/console_preamble.js new file mode 100644 index 0000000000..151f5c1e03 --- /dev/null +++ b/dtool/src/dtoolutil/console_preamble.js @@ -0,0 +1,50 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file console_preamble.js + * @author rdb + * @date 2025-02-03 + */ + +if (ENVIRONMENT_IS_NODE) { + Module["preInit"] = Module["preInit"] || []; + Module["preInit"].push(function() { + if (typeof process === "object" && typeof process.env === "object") { + // These are made up by emscripten if we don't set them to undefined + ENV['USER'] = undefined; + ENV['LOGNAME'] = undefined; + ENV['PATH'] = undefined; + ENV['PWD'] = undefined; + ENV['HOME'] = undefined; + ENV['LANG'] = undefined; + ENV['_'] = undefined; + for (var variable in process.env) { + ENV[variable] = process.env[variable]; + } + } + + addOnPreMain(function preloadNodeEnv() { + var sp = stackSave(); + var set_binary_name = wasmExports["_set_binary_name"]; + if (set_binary_name && typeof __filename === "string") { + set_binary_name(stringToUTF8OnStack(__filename)); + } + + var set_env_var = wasmExports["_set_env_var"]; + if (set_env_var) { + for (var variable in ENV) { + var value = ENV[variable]; + if (value !== undefined) { + set_env_var(stringToUTF8OnStack(variable), stringToUTF8OnStack(value)); + } + } + } + stackRestore(sp); + }); + }); +} diff --git a/dtool/src/dtoolutil/executionEnvironment.cxx b/dtool/src/dtoolutil/executionEnvironment.cxx index 71083fa7ab..371a211612 100644 --- a/dtool/src/dtoolutil/executionEnvironment.cxx +++ b/dtool/src/dtoolutil/executionEnvironment.cxx @@ -125,12 +125,18 @@ static const char *const libp3dtool_filenames[] = { #if defined(__EMSCRIPTEN__) && !defined(CPPPARSER) extern "C" void EMSCRIPTEN_KEEPALIVE -_set_env_var(ExecutionEnvironment *ptr, const char *var, const char *value) { +_set_env_var(const char *var, const char *value) { + ExecutionEnvironment *ptr = ExecutionEnvironment::get_ptr(); ptr->_variables[std::string(var)] = std::string(value); } + +extern "C" void EMSCRIPTEN_KEEPALIVE +_set_binary_name(const char *path) { + ExecutionEnvironment::set_binary_name(std::string(path)); +} #endif -// Linux with GNU libc does have global argvargc variables, but we can't +// Linux with GNU libc does have global argv/argc variables, but we can't // safely access them at stat init time--at least, not in libc5. (It does seem // to work with glibc2, however.) @@ -584,17 +590,10 @@ read_environment_variables() { } } #elif defined(__EMSCRIPTEN__) - // We only have environment variables if we're running in node.js. -#ifndef CPPPARSER - EM_ASM({ - if (typeof process === 'object' && typeof process.env === 'object') { - for (var variable in process.env) { - __set_env_var($0, stringToUTF8OnStack(variable), - stringToUTF8OnStack(process.env[variable])); - } - } - }, this); -#endif + // The environment variables get loaded in by the .js file before main() + // using the _set_env_var exported function, defined above. Trying to load + // env vars at static init time otherwise makes some optimizations more + // difficult, notably wasm-ctor-eval/wizer. #elif defined(HAVE_PROC_SELF_ENVIRON) // In some cases, we may have a file called procselfenviron that may be read diff --git a/dtool/src/dtoolutil/executionEnvironment.h b/dtool/src/dtoolutil/executionEnvironment.h index e54f87c40f..ddb83fcecc 100644 --- a/dtool/src/dtoolutil/executionEnvironment.h +++ b/dtool/src/dtoolutil/executionEnvironment.h @@ -22,10 +22,10 @@ #include #if defined(__EMSCRIPTEN__) && !defined(CPPPARSER) -class ExecutionEnvironment; - extern "C" void EMSCRIPTEN_KEEPALIVE -_set_env_var(ExecutionEnvironment *ptr, const char *var, const char *value); +_set_env_var(const char *var, const char *value); +extern "C" void EMSCRIPTEN_KEEPALIVE +_set_binary_name(const char *path); #endif /** @@ -98,7 +98,7 @@ private: static ExecutionEnvironment *_global_ptr; #ifdef __EMSCRIPTEN__ - friend void ::_set_env_var(ExecutionEnvironment *ptr, const char *var, const char *value); + friend void ::_set_env_var(const char *var, const char *value); #endif }; diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 7e4588051a..614b2cbb45 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1913,6 +1913,9 @@ def CompileLink(dll, obj, opts): if GetOrigExt(dll) == ".exe": cmd += " -s EXIT_RUNTIME=1" + if dll.endswith(".js") and "SUBSYSTEM:WINDOWS" not in opts: + cmd += " --pre-js dtool/src/dtoolutil/console_preamble.js" + else: cmd += " -pthread" if "SYSROOT" in SDK: @@ -5984,7 +5987,7 @@ if GetLinkAllStatic(): if not PkgSkip('BULLET'): DefSymbol('RUN_TESTS_FLAGS', 'HAVE_BULLET') - OPTS=['DIR:tests', 'PYTHON', 'RUN_TESTS_FLAGS'] + OPTS=['DIR:tests', 'PYTHON', 'RUN_TESTS_FLAGS', 'SUBSYSTEM:CONSOLE'] PyTargetAdd('run_tests-main.obj', opts=OPTS, input='main.c') PyTargetAdd('run_tests.exe', input='run_tests-main.obj') PyTargetAdd('run_tests.exe', input='core.pyd') diff --git a/tests/main.c b/tests/main.c index acf26dd10e..8fce2d6f92 100644 --- a/tests/main.c +++ b/tests/main.c @@ -19,11 +19,6 @@ #include -#ifdef __EMSCRIPTEN__ -#include -#include -#endif - #include "pandabase.h" #ifdef LINK_ALL_STATIC @@ -55,43 +50,6 @@ int main(int argc, char **argv) { PyConfig config; PyConfig_InitPythonConfig(&config); -#ifdef __EMSCRIPTEN__ - // getenv does not work with emscripten, instead read the PYTHONPATH and - // PYTHONHOME from the process.env variable if we're running in node.js. - char path[4096], home[4096]; - path[0] = 0; - home[0] = 0; - - EM_ASM({ - if (typeof process === 'object' && typeof process.env === 'object') { - var path = process.env.PYTHONPATH; - var home = process.env.PYTHONHOME; - if (path) { - if (process.platform === 'win32') { - path = path.replace(/;/g, ':'); - } - stringToUTF8(path, $0, $1); - } - if (home) { - stringToUTF8(home, $2, $3); - } - } - }, path, sizeof(path), home, sizeof(home)); - - if (path[0] != 0) { - status = PyConfig_SetBytesString(&config, &config.pythonpath_env, path); - if (PyStatus_Exception(status)) { - goto exception; - } - } - if (home[0] != 0) { - status = PyConfig_SetBytesString(&config, &config.home, home); - if (PyStatus_Exception(status)) { - goto exception; - } - } -#endif - PyConfig_SetBytesString(&config, &config.run_module, "pytest"); config.parse_argv = 0; From fef27a7cb0d0158a9e4ea6d08790fabadd03f639 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 3 Feb 2025 18:09:00 +0100 Subject: [PATCH 055/291] Update emscripten to 4.0.2, Python to 3.12.8, enable bulk memory --- .github/workflows/ci.yml | 20 ++++++++++---------- makepanda/makepanda.py | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18ce4881f9..04cea3eec7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -508,7 +508,7 @@ jobs: - name: Setup emsdk uses: mymindstorm/setup-emsdk@v14 with: - version: 3.1.70 + version: 4.0.2 actions-cache-folder: 'emsdk-cache' - name: Restore Python build cache @@ -516,19 +516,19 @@ jobs: uses: actions/cache/restore@v4 with: path: ~/python - key: cache-emscripten-python-3.12.7 + key: cache-emscripten-python-3.12.8 - name: Build Python 3.12 if: steps.cache-emscripten-python-restore.outputs.cache-hit != 'true' run: | - wget https://www.python.org/ftp/python/3.12.7/Python-3.12.7.tar.xz - tar -xJf Python-3.12.7.tar.xz - (cd Python-3.12.7 && EM_CONFIG=$EMSDK/.emscripten python3 Tools/wasm/wasm_build.py emscripten-browser) - (cd Python-3.12.7/builddir/emscripten-browser && make install DESTDIR=~/python) - cp Python-3.12.7/builddir/emscripten-browser/Modules/_hacl/libHacl_Hash_SHA2.a ~/python/usr/local/lib - cp Python-3.12.7/builddir/emscripten-browser/Modules/_decimal/libmpdec/libmpdec.a ~/python/usr/local/lib - cp Python-3.12.7/builddir/emscripten-browser/Modules/expat/libexpat.a ~/python/usr/local/lib - rm -rf Python-3.12.7 + wget https://www.python.org/ftp/python/3.12.8/Python-3.12.8.tar.xz + tar -xJf Python-3.12.8.tar.xz + (cd Python-3.12.8 && EM_CONFIG=$EMSDK/.emscripten python3 Tools/wasm/wasm_build.py emscripten-browser) + (cd Python-3.12.8/builddir/emscripten-browser && make install DESTDIR=~/python) + cp Python-3.12.8/builddir/emscripten-browser/Modules/_hacl/libHacl_Hash_SHA2.a ~/python/usr/local/lib + cp Python-3.12.8/builddir/emscripten-browser/Modules/_decimal/libmpdec/libmpdec.a ~/python/usr/local/lib + cp Python-3.12.8/builddir/emscripten-browser/Modules/expat/libexpat.a ~/python/usr/local/lib + rm -rf Python-3.12.8 - name: Save Python build cache id: cache-emscripten-python-save diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 614b2cbb45..89aed0d7ba 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1909,7 +1909,8 @@ def CompileLink(dll, obj, opts): cmd += ' -lc -lm' elif GetTarget() == 'emscripten': - cmd += " -s WARN_ON_UNDEFINED_SYMBOLS=1" + cmd += " -s WARN_ON_UNDEFINED_SYMBOLS=1 -mbulk-memory" + if GetOrigExt(dll) == ".exe": cmd += " -s EXIT_RUNTIME=1" From c729c75b299cb7d7945daaded6691fdf973fc458 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 5 Feb 2025 10:03:54 +0100 Subject: [PATCH 056/291] display: Add queries for max work group count/size/invocations Fixes #1713 --- panda/src/display/graphicsStateGuardian.I | 41 ++++++++++++++++++- panda/src/display/graphicsStateGuardian.cxx | 5 ++- panda/src/display/graphicsStateGuardian.h | 14 ++++++- panda/src/gles2gsg/gles2gsg.h | 3 ++ .../glstuff/glGraphicsStateGuardian_src.cxx | 31 +++++++++++++- .../src/glstuff/glGraphicsStateGuardian_src.h | 5 +++ 6 files changed, 94 insertions(+), 5 deletions(-) diff --git a/panda/src/display/graphicsStateGuardian.I b/panda/src/display/graphicsStateGuardian.I index 09dd8c60b4..b7ff6eb2db 100644 --- a/panda/src/display/graphicsStateGuardian.I +++ b/panda/src/display/graphicsStateGuardian.I @@ -596,7 +596,7 @@ get_supports_tessellation_shaders() const { */ INLINE bool GraphicsStateGuardian:: get_supports_compute_shaders() const { - return _supports_compute_shaders; + return _max_compute_work_group_invocations > 0; } /** @@ -709,6 +709,45 @@ get_supports_dual_source_blending() const { return _supports_dual_source_blending; } +/** + * Returns the maximum number of work groups that can be submitted in a single + * compute dispatch. + * + * If compute shaders are supported, this will be at least 65535x65535x65535. + * Otherwise, it will be zero. + */ +INLINE LVecBase3i GraphicsStateGuardian:: +get_max_compute_work_group_count() const { + return _max_compute_work_group_count; +} + +/** + * Returns the maximum number of invocations in each work group split out + * separately to every x, y, z dimension. This limit applies in addition to + * the overall number of invocations, which is specified by + * get_max_compute_work_group_invocations(). + * + * If compute shaders are supported, this will be at least 128x128x64. + * Otherwise, it will be zero. + */ +INLINE LVecBase3i GraphicsStateGuardian:: +get_max_compute_work_group_size() const { + return _max_compute_work_group_size; +} + +/** + * Returns the maximum number of invocations in each work group as a product + * of the x, y, z dimensions. This limit applies in addition to the + * per-dimension limits specified by get_max_compute_work_group_size(). + * + * If compute shaders are supported, this will be at least 128. Otherwise, it + * will be zero. + */ +INLINE int GraphicsStateGuardian:: +get_max_compute_work_group_invocations() const { + return _max_compute_work_group_invocations; +} + /** * Deprecated. Use get_max_color_targets() instead, which returns the exact * same value. diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 80c02f88bb..14fb75a187 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -244,10 +244,13 @@ GraphicsStateGuardian(CoordinateSystem internal_coordinate_system, _supports_basic_shaders = false; _supports_geometry_shaders = false; _supports_tessellation_shaders = false; - _supports_compute_shaders = false; _supports_glsl = false; _supports_hlsl = false; + _max_compute_work_group_count = LVecBase3i(0, 0, 0); + _max_compute_work_group_size = LVecBase3i(0, 0, 0); + _max_compute_work_group_invocations = 0; + _supports_stencil = false; _supports_stencil_wrap = false; _supports_two_sided_stencil = false; diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index 039572a7af..2c24bf5aa4 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -176,6 +176,12 @@ PUBLISHED: INLINE int get_maximum_simultaneous_render_targets() const; INLINE bool get_supports_dual_source_blending() const; +public: + INLINE LVecBase3i get_max_compute_work_group_count() const; + INLINE LVecBase3i get_max_compute_work_group_size() const; + INLINE int get_max_compute_work_group_invocations() const; + +PUBLISHED: MAKE_PROPERTY(max_vertices_per_array, get_max_vertices_per_array); MAKE_PROPERTY(max_vertices_per_primitive, get_max_vertices_per_primitive); MAKE_PROPERTY(max_texture_stages, get_max_texture_stages); @@ -222,6 +228,9 @@ PUBLISHED: MAKE_PROPERTY(timer_queries_active, get_timer_queries_active); MAKE_PROPERTY(max_color_targets, get_max_color_targets); MAKE_PROPERTY(supports_dual_source_blending, get_supports_dual_source_blending); + MAKE_PROPERTY(max_compute_work_group_count, get_max_compute_work_group_count); + MAKE_PROPERTY(max_compute_work_group_size, get_max_compute_work_group_size); + MAKE_PROPERTY(max_compute_work_group_invocations, get_max_compute_work_group_invocations); INLINE ShaderModel get_shader_model() const; INLINE void set_shader_model(ShaderModel shader_model); @@ -620,12 +629,15 @@ protected: bool _supports_basic_shaders; bool _supports_geometry_shaders; bool _supports_tessellation_shaders; - bool _supports_compute_shaders; bool _supports_glsl; bool _supports_hlsl; bool _supports_framebuffer_multisample; bool _supports_framebuffer_blit; + LVecBase3i _max_compute_work_group_count; + LVecBase3i _max_compute_work_group_size; + int _max_compute_work_group_invocations; + bool _supports_stencil; bool _supports_stencil_wrap; bool _supports_two_sided_stencil; diff --git a/panda/src/gles2gsg/gles2gsg.h b/panda/src/gles2gsg/gles2gsg.h index feb30b41a0..48d8695586 100644 --- a/panda/src/gles2gsg/gles2gsg.h +++ b/panda/src/gles2gsg/gles2gsg.h @@ -278,6 +278,7 @@ typedef char GLchar; #define GL_UNSIGNED_INT_IMAGE_BUFFER 0x9067 #define GL_UNSIGNED_INT_IMAGE_2D_ARRAY 0x9069 #define GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY 0x906A +#define GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 0x90EB #define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117 #define GL_UNSIGNALED 0x9118 #define GL_SIGNALED 0x9119 @@ -285,6 +286,8 @@ typedef char GLchar; #define GL_TIMEOUT_EXPIRED 0x911B #define GL_CONDITION_SATISFIED 0x911C #define GL_COMPUTE_SHADER 0x91B9 +#define GL_MAX_COMPUTE_WORK_GROUP_COUNT 0x91BE +#define GL_MAX_COMPUTE_WORK_GROUP_SIZE 0x91BF #define GL_FRAMEBUFFER_DEFAULT_WIDTH 0x9310 #define GL_FRAMEBUFFER_DEFAULT_HEIGHT 0x9311 #define GL_FRAMEBUFFER_DEFAULT_SAMPLES 0x9313 diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 8bea9aa06b..682717a55f 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -936,6 +936,15 @@ reset() { // Print out a list of all extensions. report_extensions(); +#ifndef OPENGLES_1 + if (_gl_version_major >= 3) { + _glGetIntegeri_v = (PFNGLGETINTEGERI_VPROC) + get_extension_func("glGetIntegeri_v"); + } else { + _glGetIntegeri_v = nullptr; + } +#endif + // Check if we are running under a profiling tool such as apitrace. #if !defined(NDEBUG) && !defined(OPENGLES_1) if (has_extension("GL_EXT_debug_marker")) { @@ -2116,7 +2125,6 @@ reset() { } #endif // HAVE_CG - _supports_compute_shaders = false; #ifndef OPENGLES_1 #ifdef OPENGLES if (is_at_least_gles_version(3, 1)) { @@ -2127,7 +2135,26 @@ reset() { get_extension_func("glDispatchCompute"); if (_glDispatchCompute != nullptr) { - _supports_compute_shaders = true; + glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &_max_compute_work_group_invocations); + + if (_max_compute_work_group_invocations > 0) { + // Initialize to spec-mandated minima + _max_compute_work_group_count.fill(65535); +#ifdef OPENGLES + _max_compute_work_group_size.set(128, 128, 64); +#else + _max_compute_work_group_size.set(1024, 1024, 64); +#endif + + if (_glGetIntegeri_v != nullptr) { + _glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0, &_max_compute_work_group_count[0]); + _glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 1, &_max_compute_work_group_count[1]); + _glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 2, &_max_compute_work_group_count[2]); + _glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 0, &_max_compute_work_group_size[0]); + _glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 1, &_max_compute_work_group_size[1]); + _glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 2, &_max_compute_work_group_size[2]); + } + } } } #endif // !OPENGLES_1 diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index a355962005..98bd8d3508 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -168,6 +168,7 @@ typedef void (APIENTRYP PFNGLGETPROGRAMINFOLOGPROC) (GLuint program, GLsizei buf typedef void (APIENTRYP PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params); typedef void (APIENTRYP PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog); typedef GLint (APIENTRYP PFNGLGETUNIFORMLOCATIONPROC) (GLuint program, const GLchar *name); +typedef void (APIENTRYP PFNGLGETINTEGERI_VPROC) (GLenum target, GLuint index, GLint *data); typedef void (APIENTRYP PFNGLLINKPROGRAMPROC) (GLuint program); typedef void (APIENTRYP PFNGLSHADERSOURCEPROC_P) (GLuint shader, GLsizei count, const GLchar* const *string, const GLint *length); typedef void (APIENTRYP PFNGLUSEPROGRAMPROC) (GLuint program); @@ -823,6 +824,10 @@ protected: #endif public: +#ifndef OPENGLES_1 + PFNGLGETINTEGERI_VPROC _glGetIntegeri_v; +#endif + #ifndef OPENGLES_1 bool _use_depth_zero_to_one; bool _use_remapped_depth_range; From a7f8c57681522dedfb7d24290c98f3b09bed2a07 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 5 Feb 2025 10:12:28 +0100 Subject: [PATCH 057/291] display: Add per-shader PStats timer for compute dispatches Also add a total work group counter Fixes #1721 --- panda/src/display/graphicsStateGuardian.cxx | 3 +++ panda/src/display/graphicsStateGuardian.h | 1 + panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 9 +++++++-- panda/src/glstuff/glShaderContext_src.cxx | 4 ++++ panda/src/glstuff/glShaderContext_src.h | 4 ++++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 14fb75a187..d94c7b5972 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -100,6 +100,7 @@ PStatCollector GraphicsStateGuardian::_draw_primitive_pcollector("Draw:Primitive PStatCollector GraphicsStateGuardian::_draw_set_state_pcollector("Draw:Set State"); PStatCollector GraphicsStateGuardian::_flush_pcollector("Draw:Flush"); PStatCollector GraphicsStateGuardian::_compute_dispatch_pcollector("Draw:Compute dispatch"); +PStatCollector GraphicsStateGuardian::_compute_work_groups_pcollector("Compute work groups"); PStatCollector GraphicsStateGuardian::_wait_occlusion_pcollector("Wait:Occlusion"); PStatCollector GraphicsStateGuardian::_wait_timer_pcollector("Wait:Timer Queries"); @@ -2780,6 +2781,7 @@ end_frame(Thread *current_thread) { _vertices_tri_pcollector.flush_level(); _vertices_patch_pcollector.flush_level(); _vertices_other_pcollector.flush_level(); + _compute_work_groups_pcollector.flush_level(); _state_pcollector.flush_level(); _texture_state_pcollector.flush_level(); @@ -3428,6 +3430,7 @@ init_frame_pstats() { _vertices_tri_pcollector.clear_level(); _vertices_patch_pcollector.clear_level(); _vertices_other_pcollector.clear_level(); + _compute_work_groups_pcollector.clear_level(); _state_pcollector.clear_level(); _transform_state_pcollector.clear_level(); diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index 2c24bf5aa4..9cb390f88c 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -706,6 +706,7 @@ public: static PStatCollector _draw_set_state_pcollector; static PStatCollector _flush_pcollector; static PStatCollector _compute_dispatch_pcollector; + static PStatCollector _compute_work_groups_pcollector; static PStatCollector _wait_occlusion_pcollector; static PStatCollector _wait_timer_pcollector; static PStatCollector _timer_queries_pcollector; diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 682717a55f..f95dcc7467 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -7916,11 +7916,16 @@ void CLP(GraphicsStateGuardian):: dispatch_compute(int num_groups_x, int num_groups_y, int num_groups_z) { maybe_gl_finish(); - PStatGPUTimer timer(this, _compute_dispatch_pcollector); - nassertv(_supports_compute_shaders); + nassertv(get_supports_compute_shaders()); nassertv(_current_shader_context != nullptr); CLP(ShaderContext) *gsc; DCAST_INTO_V(gsc, _current_shader_context); + +#ifdef DO_PSTATS + _compute_work_groups_pcollector.add_level(num_groups_x * num_groups_y * num_groups_z); + PStatGPUTimer timer(this, gsc->_compute_dispatch_pcollector); +#endif + gsc->issue_memory_barriers(); _glDispatchCompute(num_groups_x, num_groups_y, num_groups_z); diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx index f928882d2f..169ae1b670 100644 --- a/panda/src/glstuff/glShaderContext_src.cxx +++ b/panda/src/glstuff/glShaderContext_src.cxx @@ -421,6 +421,10 @@ CLP(ShaderContext)(CLP(GraphicsStateGuardian) *glgsg, Shader *s) : ShaderContext _mat_part_cache = new LVecBase4f[_shader->cp_get_mat_cache_size()]; _mat_scratch_space = new LVecBase4f[_shader->cp_get_mat_scratch_size()]; + +#ifdef DO_PSTATS + _compute_dispatch_pcollector = PStatCollector(glgsg->_compute_dispatch_pcollector, s->get_debug_name()); +#endif } /** diff --git a/panda/src/glstuff/glShaderContext_src.h b/panda/src/glstuff/glShaderContext_src.h index f196c1b6ba..3088170ec3 100644 --- a/panda/src/glstuff/glShaderContext_src.h +++ b/panda/src/glstuff/glShaderContext_src.h @@ -120,6 +120,10 @@ private: bool _uses_standard_vertex_arrays; +#ifdef DO_PSTATS + PStatCollector _compute_dispatch_pcollector; +#endif + void glsl_report_shader_errors(GLuint shader, Shader::ShaderType type, bool fatal); void glsl_report_program_errors(GLuint program, bool fatal); bool glsl_compile_shader(Shader::ShaderType type); From 8de98592ca49a55bc71ca6cd934e3f0b8a74cf80 Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Wed, 19 Feb 2025 10:59:19 -0700 Subject: [PATCH 058/291] showbase: Annotate basic `Loader` methods (#1723) --- direct/src/showbase/Loader.py | 72 +++++++++++++++++++++++++---------- tests/showbase/test_Loader.py | 4 +- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index 7febb9012d..6e49ebf53c 100644 --- a/direct/src/showbase/Loader.py +++ b/direct/src/showbase/Loader.py @@ -2,9 +2,13 @@ sound, music, shaders and fonts from disk. """ +from __future__ import annotations + __all__ = ['Loader'] from panda3d.core import ( + AsyncTask, + AudioManager, ConfigVariableBool, Filename, FontPool, @@ -24,13 +28,15 @@ from panda3d.core import ( from panda3d.core import Loader as PandaLoader from direct.directnotify.DirectNotifyGlobal import directNotify from direct.showbase.DirectObject import DirectObject +from . import ShowBase import warnings import sys +from typing import Any, Callable, Iterable # You can specify a phaseChecker callback to check # a modelPath to see if it is being loaded in the correct # phase -phaseChecker = None +phaseChecker: Callable[[str, LoaderOptions], object] | None = None class Loader(DirectObject): @@ -49,16 +55,23 @@ class Loader(DirectObject): # This indicates that this class behaves like a Future. _asyncio_future_blocking = False - def __init__(self, loader, numObjects, gotList, callback, extraArgs): + def __init__( + self, + loader: Loader | None, + numObjects: int, + gotList: bool, + callback: Callable[..., object] | None, + extraArgs: list, + ) -> None: self._loader = loader - self.objects = [None] * numObjects + self.objects: list[Any | None] = [None] * numObjects self.gotList = gotList self.callback = callback self.extraArgs = extraArgs - self.requests = set() - self.requestList = [] + self.requests: set[AsyncTask] = set() + self.requestList: list[AsyncTask] = [] - def gotObject(self, index, object): + def gotObject(self, index: int, object) -> None: self.objects[index] = object if not self.requests: @@ -79,7 +92,7 @@ class Loader(DirectObject): self.requests = None self.requestList = None - def cancelled(self): + def cancelled(self) -> bool: "Returns true if the request was cancelled." return self.requestList is None @@ -87,7 +100,7 @@ class Loader(DirectObject): "Returns true if all the requests were finished or cancelled." return not self.requests - def result(self): + def result(self) -> Any: "Returns the results, suspending the thread to wait if necessary." for r in list(self.requests): r.wait() @@ -126,26 +139,26 @@ class Loader(DirectObject): yield await req # special methods - def __init__(self, base=None): + def __init__(self, base: ShowBase.ShowBase | None = None) -> None: self.base = base self.loader = PandaLoader.getGlobalPtr() - self._requests = {} + self._requests: dict[AsyncTask, tuple[Loader._Callback, int]] = {} self.hook = "async_loader_%s" % (Loader.loaderIndex) Loader.loaderIndex += 1 - def destroy(self): + def destroy(self) -> None: self.ignore(self.hook) self.loader.stopThreads() del self.base - def _init_base(self, base): + def _init_base(self, base: ShowBase.ShowBase) -> None: self.base = base self.accept(self.hook, self.__gotAsyncObject) @classmethod - def _loadPythonFileTypes(cls): + def _loadPythonFileTypes(cls) -> None: if cls._loadedPythonFileTypes: return @@ -168,10 +181,18 @@ class Loader(DirectObject): cls._loadedPythonFileTypes = True # model loading funcs - def loadModel(self, modelPath, loaderOptions = None, noCache = None, - allowInstance = False, okMissing = None, - callback = None, extraArgs = [], priority = None, - blocking = None): + def loadModel( + self, + modelPath: str | list[str] | tuple[str, ...] | set[str], + loaderOptions: LoaderOptions | None = None, + noCache: bool | None = None, + allowInstance: bool = False, + okMissing: bool | None = None, + callback: Callable[..., object] | None = None, + extraArgs: list = [], + priority: int | None = None, + blocking: bool | None = None, + ) -> Any: """ Attempts to load a model or models from one or more relative pathnames. If the input modelPath is a string (a single model @@ -255,6 +276,7 @@ class Loader(DirectObject): if allowInstance: loaderOptions.setFlags(loaderOptions.getFlags() | LoaderOptions.LFAllowInstance) + modelList: Iterable[str] if not isinstance(modelPath, (tuple, list, set)): # We were given a single model pathname. modelList = [modelPath] @@ -949,7 +971,7 @@ class Loader(DirectObject): TexturePool.releaseTexture(texture) # sound loading funcs - def loadSfx(self, *args, **kw): + def loadSfx(self, *args, **kw) -> Any: """Loads one or more sound files, specifically designated as a "sound effect" file (that is, uses the sfxManager to load the sound). There is no distinction between sound effect files @@ -959,6 +981,7 @@ class Loader(DirectObject): independently of the other group.""" # showbase-created sfxManager should always be at front of list + assert self.base is not None if self.base.sfxManagerList: return self.loadSound(self.base.sfxManagerList[0], *args, **kw) return None @@ -976,8 +999,14 @@ class Loader(DirectObject): else: return None - def loadSound(self, manager, soundPath, positional = False, - callback = None, extraArgs = []): + def loadSound( + self, + manager: AudioManager, + soundPath: str | tuple[str, ...] | list[str] | set[str], + positional: bool = False, + callback: Callable[..., object] | None = None, + extraArgs: list = [], + ) -> Any: """Loads one or more sound files, specifying the particular AudioManager that should be used to load them. The soundPath may be either a single filename, or a list of filenames. If a @@ -987,6 +1016,7 @@ class Loader(DirectObject): from panda3d.core import AudioLoadRequest + soundList: Iterable[str] if not isinstance(soundPath, (tuple, list, set)): # We were given a single sound pathname or a MovieAudio instance. soundList = [soundPath] @@ -1112,7 +1142,7 @@ class Loader(DirectObject): else: callback(*(origModelList + extraArgs)) - def __gotAsyncObject(self, request): + def __gotAsyncObject(self, request: AsyncTask) -> None: """A model or sound file or some such thing has just been loaded asynchronously by the sub-thread. Add it to the list of loaded objects, and call the appropriate callback when it's diff --git a/tests/showbase/test_Loader.py b/tests/showbase/test_Loader.py index db8812fd1f..b082ea6fe5 100644 --- a/tests/showbase/test_Loader.py +++ b/tests/showbase/test_Loader.py @@ -6,7 +6,9 @@ import sys @pytest.fixture def loader(): - return Loader(base=None) + loader = Loader(base=None) + yield loader + loader.destroy() @pytest.fixture From f4da124b501b7a702d0469f3be249ed01f50c2dd Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 22 Feb 2025 11:06:31 +0100 Subject: [PATCH 059/291] pgraphnodes: Add setter for shadow buffer sort --- panda/src/pgraphnodes/lightLensNode.I | 8 ++++++++ panda/src/pgraphnodes/lightLensNode.h | 2 ++ 2 files changed, 10 insertions(+) diff --git a/panda/src/pgraphnodes/lightLensNode.I b/panda/src/pgraphnodes/lightLensNode.I index 4fd1138b03..ccdb55ea0a 100644 --- a/panda/src/pgraphnodes/lightLensNode.I +++ b/panda/src/pgraphnodes/lightLensNode.I @@ -36,6 +36,14 @@ get_shadow_buffer_sort() const { return _sb_sort; } +/** + * Sets the sort of the shadow buffer to be created for this light source. + */ +INLINE void LightLensNode:: +set_shadow_buffer_sort(int sort) { + _sb_sort = sort; +} + /** * Returns the size of the shadow buffer to be created for this light source. */ diff --git a/panda/src/pgraphnodes/lightLensNode.h b/panda/src/pgraphnodes/lightLensNode.h index f045c539b3..1d6b7720c8 100644 --- a/panda/src/pgraphnodes/lightLensNode.h +++ b/panda/src/pgraphnodes/lightLensNode.h @@ -42,6 +42,7 @@ PUBLISHED: void set_shadow_caster(bool caster, int buffer_xsize, int buffer_ysize, int sort = -10); INLINE int get_shadow_buffer_sort() const; + INLINE void set_shadow_buffer_sort(int sort); INLINE LVecBase2i get_shadow_buffer_size() const; INLINE void set_shadow_buffer_size(const LVecBase2i &size); @@ -50,6 +51,7 @@ PUBLISHED: PUBLISHED: MAKE_PROPERTY(shadow_caster, is_shadow_caster); + MAKE_PROPERTY(shadow_buffer_sort, get_shadow_buffer_sort, set_shadow_buffer_sort); MAKE_PROPERTY(shadow_buffer_size, get_shadow_buffer_size, set_shadow_buffer_size); public: From ab57b3c72f3cdacf6a7bbbcc29c94afff5825572 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 24 Feb 2025 20:06:28 +0100 Subject: [PATCH 060/291] downloader: Fix HTTP downloads on latest emscripten --- panda/src/downloader/httpChannel_emscripten.cxx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/panda/src/downloader/httpChannel_emscripten.cxx b/panda/src/downloader/httpChannel_emscripten.cxx index 859166fbb7..3054d065f7 100644 --- a/panda/src/downloader/httpChannel_emscripten.cxx +++ b/panda/src/downloader/httpChannel_emscripten.cxx @@ -373,9 +373,11 @@ run_headers_received() { var xhr = window._httpChannels[$0]; var loaded = 0; xhr.onprogress = function (ev) { - var chunk = this.responseText.slice(loaded, ev.loaded); - var ptr = __extend_string($1, chunk.length); - writeAsciiToMemory(chunk, ptr, true); + var body = this.responseText; + var ptr = __extend_string($1, ev.loaded - loaded); + for (var i = loaded; i < ev.loaded; ++i) { + HEAPU8[ptr + i] = body.charCodeAt(i); + } loaded = ev.loaded; }; }, this, dest); @@ -388,8 +390,11 @@ run_headers_received() { var loaded = 0; xhr.onprogress = function (ev) { while (loaded < ev.loaded) { + var body = this.responseText; var size = Math.min(ev.loaded - read, 4096); - writeAsciiToMemory(this.responseText.substr(read, size), $2, true); + for (var i = read; i < read + size; ++i) { + HEAPU8[$2 + i] = body.charCodeAt(i); + } __write_stream($1, $2, size); loaded += size; } @@ -700,7 +705,9 @@ download_to_ram(Ramfile *ramfile, bool subdocument_resumes) { var state = xhr.readyState; var body = xhr.responseText; var ptr = __extend_string($1, body.length); - writeAsciiToMemory(body, ptr, true); + for (var i = 0; i < body.length; ++i) { + HEAPU8[ptr + i] = body.charCodeAt(i); + } return state; }, this, &ramfile->_data); From c960874d4ee06303ff7ea439d0f6abe44b70ca77 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 24 Feb 2025 20:07:31 +0100 Subject: [PATCH 061/291] downloader: Add url property to VirtualFileHTTP --- panda/src/downloader/virtualFileHTTP.cxx | 13 +++++++++++-- panda/src/downloader/virtualFileHTTP.h | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/panda/src/downloader/virtualFileHTTP.cxx b/panda/src/downloader/virtualFileHTTP.cxx index ee36ea95bd..d96a948ae4 100644 --- a/panda/src/downloader/virtualFileHTTP.cxx +++ b/panda/src/downloader/virtualFileHTTP.cxx @@ -39,8 +39,7 @@ VirtualFileHTTP(VirtualFileMountHTTP *mount, const Filename &local_filename, _implicit_pz_file(implicit_pz_file), _status_only(open_flags != 0) { - URLSpec url(_mount->get_root()); - url.set_path(_mount->get_root().get_path() + _local_filename.c_str()); + URLSpec url = get_url(); _channel = _mount->get_channel(); if (_status_only) { _channel->get_header(url); @@ -89,6 +88,16 @@ get_filename() const { } } +/** + * Returns the full URL of this file. + */ +URLSpec VirtualFileHTTP:: +get_url() const { + URLSpec url(_mount->get_root()); + url.set_path(_mount->get_root().get_path() + _local_filename.c_str()); + return url; +} + /** * Returns true if this file exists, false otherwise. */ diff --git a/panda/src/downloader/virtualFileHTTP.h b/panda/src/downloader/virtualFileHTTP.h index 49e5f4bd19..ed9bdfa04e 100644 --- a/panda/src/downloader/virtualFileHTTP.h +++ b/panda/src/downloader/virtualFileHTTP.h @@ -39,6 +39,7 @@ public: virtual VirtualFileSystem *get_file_system() const; virtual Filename get_filename() const; + URLSpec get_url() const; virtual bool has_file() const; virtual bool is_directory() const; @@ -54,6 +55,9 @@ public: virtual bool read_file(std::string &result, bool auto_unwrap) const; virtual bool read_file(vector_uchar &result, bool auto_unwrap) const; +PUBLISHED: + MAKE_PROPERTY(url, get_url); + private: bool fetch_file(std::ostream *buffer_stream) const; std::istream *return_file(std::istream *buffer_stream, bool auto_unwrap) const; From 85541de504840c076026202a57760c9a3c01be66 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 24 Feb 2025 22:45:48 +0100 Subject: [PATCH 062/291] grutil: Add support for playing video via HTML video+WebGL --- .../glstuff/glGraphicsStateGuardian_src.cxx | 28 + panda/src/grutil/config_grutil.cxx | 7 + panda/src/grutil/htmlVideoTexture.I | 12 + panda/src/grutil/htmlVideoTexture.cxx | 489 ++++++++++++++++++ panda/src/grutil/htmlVideoTexture.h | 115 ++++ panda/src/grutil/p3grutil_composite1.cxx | 1 + 6 files changed, 652 insertions(+) create mode 100644 panda/src/grutil/htmlVideoTexture.I create mode 100644 panda/src/grutil/htmlVideoTexture.cxx create mode 100644 panda/src/grutil/htmlVideoTexture.h diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index f95dcc7467..6ba7a5e01d 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -70,6 +70,10 @@ #include "displayInformation.h" #include "completionCounter.h" +#ifdef __EMSCRIPTEN__ +#include "htmlVideoTexture.h" +#endif + #if defined(HAVE_CG) && !defined(OPENGLES) #include #endif @@ -14776,6 +14780,30 @@ upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload, image_ptr = ptimage; } if (image_ptr == nullptr) { +#ifdef __EMSCRIPTEN__ + if (tex->is_of_type(HTMLVideoTexture::get_class_type())) { + // Load directly from the video element into WebGL. + int result = EM_ASM_INT({ + var video = window._htmlVideoData[$7].video; + if (video.readyState < video.HAVE_CURRENT_DATA) { + return 0; + } + GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, true); + if ($8) { + GLctx.texImage2D($0, $1, $2, $3, $4, 0, $5, $6, video); + } else { + GLctx.texSubImage2D($0, $1, 0, 0, $3, $4, $5, $6, video); + } + GLctx.pixelStorei(GLctx.UNPACK_FLIP_Y_WEBGL, false); + return 1; + }, target, 0, internal_format, width, height, external_format, component_type, tex, needs_reload); + + if (result) { + continue; + } + } +#endif + if (level < num_ram_mipmap_levels) { // We were told we'd have this many RAM mipmap images, but we // don't. Raise a warning. diff --git a/panda/src/grutil/config_grutil.cxx b/panda/src/grutil/config_grutil.cxx index 6f74fd5e1b..fea92c6673 100644 --- a/panda/src/grutil/config_grutil.cxx +++ b/panda/src/grutil/config_grutil.cxx @@ -17,6 +17,7 @@ #include "meshDrawer.h" #include "meshDrawer2D.h" #include "geoMipTerrain.h" +#include "htmlVideoTexture.h" #include "movieTexture.h" #include "pandaSystem.h" #include "texturePool.h" @@ -137,4 +138,10 @@ init_libgrutil() { TexturePool *ts = TexturePool::get_global_ptr(); ts->register_texture_type(MovieTexture::make_texture, "avi m2v mov mpg mpeg mp4 wmv asf flv nut ogm mkv ogv webm"); #endif + +#ifdef __EMSCRIPTEN__ + HTMLVideoTexture::init_type(); + + ts->register_texture_type(HTMLVideoTexture::make_texture, "avi m2v mov mpg mpeg mp4 wmv asf flv nut ogm mkv ogv webm"); +#endif } diff --git a/panda/src/grutil/htmlVideoTexture.I b/panda/src/grutil/htmlVideoTexture.I new file mode 100644 index 0000000000..6feecd77ab --- /dev/null +++ b/panda/src/grutil/htmlVideoTexture.I @@ -0,0 +1,12 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file htmlVideoTexture.I + * @author rdb + * @date 2025-02-24 + */ diff --git a/panda/src/grutil/htmlVideoTexture.cxx b/panda/src/grutil/htmlVideoTexture.cxx new file mode 100644 index 0000000000..cc083ea225 --- /dev/null +++ b/panda/src/grutil/htmlVideoTexture.cxx @@ -0,0 +1,489 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file htmlVideoTexture.cxx + * @author rdb + * @date 2025-02-24 + */ + +#include "htmlVideoTexture.h" + +#ifdef __EMSCRIPTEN__ + +#include "config_grutil.h" +#include "virtualFileSystem.h" +#include "virtualFileHTTP.h" + +#include + +#ifndef CPPPARSER +// If the browser supports video frame callback, we use that instead of relying +// on cull callbacks. +extern "C" void EMSCRIPTEN_KEEPALIVE +_video_frame_callback(HTMLVideoTexture *texture, int width, int height, double time) { + texture->video_frame_callback(width, height, time); +} + +static bool get_supports_video_frame_callback() { + static bool supported = EM_ASM_INT({ + return 'requestVideoFrameCallback' in HTMLVideoElement.prototype; + }); + return supported; +} +#endif + +TypeHandle HTMLVideoTexture::_type_handle; + +/** + * Creates a blank movie texture. Movies must be added using do_read_one. + */ +HTMLVideoTexture:: +HTMLVideoTexture(const std::string &name) : + Texture(name) +{ + EM_ASM_INT({ + if (!window._htmlVideoData) { + window._htmlVideoData = {}; + } + var video = document.createElement("video"); + video.style.display = 'none'; + video.defaultMuted = true; + document.body.appendChild(video); + window._htmlVideoData[$0] = {video: video}; + }, this); +} + +/** + * xxx + */ +HTMLVideoTexture:: +~HTMLVideoTexture() { + clear(); + + EM_ASM_INT({ + var data = window._htmlVideoData[$0]; + if (data) { + if (data.video) { + if (data.callback) { + video.cancelVideoFrameCallback(data.callback); + delete data.callback; + } + data.video.remove(); + delete data.video; + } + if (data.objectURL) { + URL.revokeObjectURL(data.objectURL); + delete data.objectURL; + } + delete window._htmlVideoData[$0]; + } + }, this); +} + +/** + * Returns the length of the video. + */ +double HTMLVideoTexture:: +get_video_length() const { + return EM_ASM_DOUBLE({ + return window._htmlVideoData[$0].video.duration; + }, this); +} + +/** + * Returns the width in texels of the source video stream. This is not + * necessarily the width of the actual texture, since the texture may have + * been expanded to raise it to a power of 2. + */ +int HTMLVideoTexture:: +get_video_width() const { + return EM_ASM_DOUBLE({ + return window._htmlVideoData[$0].video.videoWidth; + }, this); +} + +/** + * Returns the height in texels of the source video stream. This is not + * necessarily the height of the actual texture, since the texture may have + * been expanded to raise it to a power of 2. + */ +int HTMLVideoTexture:: +get_video_height() const { + return EM_ASM_DOUBLE({ + return window._htmlVideoData[$0].video.videoHeight; + }, this); +} + +/** + * Start playing the video from where it was last paused. Has no effect if + * the video is not paused, or if the video's cursor is already at the end. + */ +void HTMLVideoTexture:: +restart() { + EM_ASM({ + window._htmlVideoData[$0].video.play(); + }, this); +} + +/** + * Stops a currently playing or looping movie right where it is. the video's + * cursor remains frozen at the point where it was stopped. + */ +void HTMLVideoTexture:: +stop() { + EM_ASM({ + window._htmlVideoData[$0].video.pause(); + }, this); +} + +/** + * Plays the video from the beginning. + */ +void HTMLVideoTexture:: +play() { + EM_ASM({ + var video = window._htmlVideoData[$0].video; + if (video.playing) { + video.pause(); + } + video.currentTime = 0.0; + video.play(); + }, this); +} + +/** + * Sets the video's cursor. + */ +void HTMLVideoTexture:: +set_time(double t) { + EM_ASM({ + window._htmlVideoData[$0].video.currentTime = $1; + }, this, t); +} + +/** + * Returns the current value of the video's cursor. + */ +double HTMLVideoTexture:: +get_time() const { + return EM_ASM_DOUBLE({ + return window._htmlVideoData[$0].video.currentTime; + }, this); +} + +/** + * If true, sets the video's loop count to 1 billion. If false, sets the + * movie's loop count to one. + */ +void HTMLVideoTexture:: +set_loop(bool loop) { + EM_ASM({ + window._htmlVideoData[$0].video.loop = $1; + }, this, loop); +} + +/** + * Returns true if the video's is looping. + */ +bool HTMLVideoTexture:: +get_loop() const { + return EM_ASM_INT({ + return window._htmlVideoData[$0].video.loop; + }, this); +} + +/** + * Sets the video's play-rate. This is the speed at which the video's cursor + * advances. The default is to advance 1.0 movie-seconds per real-time + * second. + */ +void HTMLVideoTexture:: +set_play_rate(double rate) { + EM_ASM({ + window._htmlVideoData[$0].video.playbackRate = $1; + }, this, rate); +} + +/** + * Gets the video's play-rate. + */ +double HTMLVideoTexture:: +get_play_rate() const { + return EM_ASM_DOUBLE({ + return window._htmlVideoData[$0].video.playbackRate; + }, this); +} + +/** + * Returns true if the video's cursor is advancing. + */ +bool HTMLVideoTexture:: +is_playing() const { + return EM_ASM_INT({ + return !window._htmlVideoData[$0].video.paused; + }, this); +} + +/** + * A factory function to make a new HTMLVideoTexture, used to pass to the + * TexturePool. + */ +PT(Texture) HTMLVideoTexture:: +make_texture() { + return new HTMLVideoTexture(""); +} + +/** + * Should be overridden by derived classes to return true if cull_callback() + * has been defined. Otherwise, returns false to indicate cull_callback() + * does not need to be called for this node during the cull traversal. + */ +bool HTMLVideoTexture:: +has_cull_callback() const { + return !get_supports_video_frame_callback(); +} + +/** + * This function will be called during the cull traversal to update the + * HTMLVideoTexture. This will just check whether the video time changed since + * the last time it was checked. + */ +bool HTMLVideoTexture:: +cull_callback(CullTraverser *, const CullTraverserData &) const { + ((HTMLVideoTexture *)this)->check_update(); + return true; +} + +/** + * + */ +void HTMLVideoTexture:: +video_frame_callback(int width, int height, double time) { + CDWriter cdata(_cycler, true); + if (width != cdata->_x_size || height != cdata->_y_size) { + cdata->_x_size = width; + cdata->_y_size = height; + cdata->inc_properties_modified(); + } + cdata->inc_image_modified(); + _last_time = time; +} + +/** + * + */ +bool HTMLVideoTexture:: +check_update() { + double current_time = get_time(); + if (current_time != _last_time) { + int width = get_video_width(); + int height = get_video_height(); + if (width == 0 && height == 0) { + return false; + } + + video_frame_callback(width, height, current_time); + return true; + } + return false; +} + +/** + * The protected implementation of clear(). Assumes the lock is already held. + */ +void HTMLVideoTexture:: +do_clear(Texture::CData *cdata) { + EM_ASM_INT({ + var data = window._htmlVideoData[$0]; + data.video.pause(); + data.video.src = ""; + if (data.callback) { + data.video.cancelVideoFrameCallback(data.callback); + delete data.callback; + } + if (data.objectURL) { + URL.revokeObjectURL(data.objectURL); + delete data.objectURL; + } + }, this); + + Texture::do_clear(cdata); +} + +/** + * Returns true if we can safely call do_unlock_and_reload_ram_image() in + * order to make the image available, or false if we shouldn't do this + * (because we know from a priori knowledge that it wouldn't work anyway). + */ +bool HTMLVideoTexture:: +do_can_reload(const Texture::CData *cdata) const { + return false; +} + +/** + * Works like adjust_size, but also considers the texture class. Movie + * textures, for instance, always pad outwards, never scale down. + */ +bool HTMLVideoTexture:: +do_adjust_this_size(const Texture::CData *cdata_tex, + int &x_size, int &y_size, const std::string &name, + bool for_padding) const { + // We always scale, for now. May change in the future. + AutoTextureScale ats = do_get_auto_texture_scale(cdata_tex); + if (ats == ATS_pad) { + ats = ATS_up; + } + + return adjust_size(x_size, y_size, name, for_padding, ats); +} + +/** + * Combines a color and alpha video image from the two indicated filenames. + * Both must be the same kind of video with similar properties. + */ +bool HTMLVideoTexture:: +do_read_one(Texture::CData *cdata_tex, + const Filename &fullpath, const Filename &alpha_fullpath, + int z, int n, int primary_file_num_channels, int alpha_file_channel, + const LoaderOptions &options, + bool header_only, BamCacheRecord *record) { + nassertr(n == 0, false); + if (!do_reconsider_z_size(cdata_tex, z, options)) { + return false; + } + nassertr(z >= 0 && z < cdata_tex->_z_size * cdata_tex->_num_views, false); + + if (!alpha_fullpath.empty()) { + grutil_cat.error() + << "HTMLVideoTexture does not support loading separate alpha video.\n"; + return false; + } + + VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr(); + PT(VirtualFile) vfile = vfs->get_file(fullpath, true); + if (vfile == nullptr) { + return false; + } + + clear(); + + if (record != nullptr) { + record->add_dependent_file(vfile); + } + + // Pass in the URL directly if it's an HTTP file. + if (vfile->is_of_type(VirtualFileHTTP::get_class_type())) { + URLSpec url = ((VirtualFileHTTP *)vfile.p())->get_url(); + + _last_time = EM_ASM_DOUBLE({ + var data = window._htmlVideoData[$0]; + var video = data.video; + video.src = UTF8ToString($1); + video.loop = true; + video.muted = true; + + video.load(); + return video.currentTime; + }, this, url.c_str()); + } + else { + vector_uchar data; + if (!vfile->read_file(data, true)) { + return false; + } + + _last_time = EM_ASM_DOUBLE({ + var data = window._htmlVideoData[$0]; + + var blob = new Blob([HEAPU8.subarray($1, $1 + $2)]); + data.objectURL = URL.createObjectURL(blob); + + var video = data.video; + video.src = data.objectURL; + video.loop = true; + video.muted = true; + + video.load(); + return video.currentTime; + }, this, (void *)data.data(), data.size()); + } + + // If the browser supports requestVideoframeCallback, we set up a callback to + // know when the next video frame is available instead of having to poll in + // cull_callback(). + if (get_supports_video_frame_callback()) { + EM_ASM({ + var data = window._htmlVideoData[$0]; + + function callback(now, metadata) { + __video_frame_callback($0, metadata.width, metadata.height, metadata.mediaTime); + data.callback = data.video.requestVideoFrameCallback(callback); + } + data.callback = data.video.requestVideoFrameCallback(callback); + }, this); + } + + int width = get_video_width(); + int height = get_video_height(); + if (width == 0 || height == 0) { + // Not yet known. Don't set the size to 0 since that will cause + // CardMaker::set_uv_range() to generate invalid coordinates. + width = 1; + height = 1; + } + + if (!do_reconsider_image_properties(cdata_tex, width, height, 3, T_unsigned_byte, z, options)) { + return false; + } + + if (z == 0) { + if (!has_name()) { + set_name(fullpath.get_basename_wo_extension()); + } + // Don't use has_filename() here, it will cause a deadlock + if (cdata_tex->_filename.empty()) { + cdata_tex->_filename = fullpath; + cdata_tex->_alpha_filename = alpha_fullpath; + } + + cdata_tex->_fullpath = fullpath; + cdata_tex->_alpha_fullpath = alpha_fullpath; + } + + cdata_tex->_primary_file_num_channels = primary_file_num_channels; + cdata_tex->_alpha_file_channel = alpha_file_channel; + + cdata_tex->_loaded_from_image = true; + play(); + check_update(); + return true; +} + +/** + * Loading a static image into an HTMLVideoTexture is an error. + */ +bool HTMLVideoTexture:: +do_load_one(Texture::CData *cdata_tex, + const PNMImage &pnmimage, const std::string &name, int z, int n, + const LoaderOptions &options) { + grutil_cat.error() << "You cannot load a static image into an HTMLVideoTexture\n"; + return false; +} + +/** + * Loading a static image into an HTMLVideoTexture is an error. + */ +bool HTMLVideoTexture:: +do_load_one(Texture::CData *cdata_tex, + const PfmFile &pfm, const std::string &name, int z, int n, + const LoaderOptions &options) { + grutil_cat.error() << "You cannot load a static image into an HTMLVideoTexture\n"; + return false; +} + +#endif // __EMSCRIPTEN__ diff --git a/panda/src/grutil/htmlVideoTexture.h b/panda/src/grutil/htmlVideoTexture.h new file mode 100644 index 0000000000..2e0c03f561 --- /dev/null +++ b/panda/src/grutil/htmlVideoTexture.h @@ -0,0 +1,115 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file htmlVideoTexture.h + * @author rdb + * @date 2025-02-24 + */ + +#ifndef HTMLVIDEOTEXTURE_H +#define HTMLVIDEOTEXTURE_H + +#include "pandabase.h" + +#ifdef __EMSCRIPTEN__ + +/** + * A texture that renders video frames from an HTMLVideoElement. + * Interface aims to be (nearly) identical to MovieTexture, with some + * limitations: + * - No RAM access of video data is possible + * - No explicit synchronization with audio + * - No loop count + */ +class EXPCL_PANDA_GRUTIL HTMLVideoTexture : public Texture { +PUBLISHED: + explicit HTMLVideoTexture(const std::string &name); + HTMLVideoTexture(const HTMLVideoTexture ©) = delete; + virtual ~HTMLVideoTexture(); + + double get_video_length() const; + int get_video_width() const; + int get_video_height() const; + + void restart(); + void stop(); + void play(); + void set_time(double t); + double get_time() const; + void set_loop(bool enable); + bool get_loop() const; + void set_play_rate(double play_rate); + double get_play_rate() const; + bool is_playing() const; + +PUBLISHED: + MAKE_PROPERTY(video_length, get_video_length); + MAKE_PROPERTY(video_width, get_video_width); + MAKE_PROPERTY(video_height, get_video_height); + + MAKE_PROPERTY(time, get_time, set_time); + MAKE_PROPERTY(loop, get_loop, set_loop); + MAKE_PROPERTY(play_rate, get_play_rate, set_play_rate); + MAKE_PROPERTY(playing, is_playing); + +public: + static PT(Texture) make_texture(); + + virtual bool has_cull_callback() const; + virtual bool cull_callback(CullTraverser *trav, const CullTraverserData &data) const; + + void video_frame_callback(int width, int height, double time); + +protected: + bool check_update(); + + virtual void do_clear(Texture::CData *cdata); + virtual bool do_can_reload(const Texture::CData *cdata) const; + + virtual bool do_adjust_this_size(const Texture::CData *cdata, + int &x_size, int &y_size, const std::string &name, + bool for_padding) const; + + virtual bool do_read_one(Texture::CData *cdata, + const Filename &fullpath, const Filename &alpha_fullpath, + int z, int n, int primary_file_num_channels, int alpha_file_channel, + const LoaderOptions &options, + bool header_only, BamCacheRecord *record); + virtual bool do_load_one(Texture::CData *cdata, + const PNMImage &pnmimage, const std::string &name, + int z, int n, const LoaderOptions &options); + virtual bool do_load_one(Texture::CData *cdata, + const PfmFile &pfm, const std::string &name, + int z, int n, const LoaderOptions &options); + +private: + double _last_time = 0.0; + +public: + static TypeHandle get_class_type() { + return _type_handle; + } + static void init_type() { + Texture::init_type(); + register_type(_type_handle, "HTMLVideoTexture", + Texture::get_class_type()); + } + virtual TypeHandle get_type() const { + return get_class_type(); + } + virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + +private: + static TypeHandle _type_handle; +}; + +#include "htmlVideoTexture.I" + +#endif // __EMSCRIPTEN__ + +#endif diff --git a/panda/src/grutil/p3grutil_composite1.cxx b/panda/src/grutil/p3grutil_composite1.cxx index 85d1c61e50..ec5ed74be9 100644 --- a/panda/src/grutil/p3grutil_composite1.cxx +++ b/panda/src/grutil/p3grutil_composite1.cxx @@ -1,5 +1,6 @@ #include "cardMaker.cxx" #include "heightfieldTesselator.cxx" +#include "htmlVideoTexture.cxx" #include "geoMipTerrain.cxx" #include "shaderTerrainMesh.cxx" #include "config_grutil.cxx" From 7c16720bb9b85199d2f175c5eede0cadc94c4d5b Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 25 Feb 2025 12:11:00 +0100 Subject: [PATCH 063/291] webgl: Improve canvas resizing behavior Automatically resize canvas to the CSS size --- .../src/webgldisplay/webGLGraphicsWindow.cxx | 76 ++++++++++++++++++- panda/src/webgldisplay/webGLGraphicsWindow.h | 3 + 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/panda/src/webgldisplay/webGLGraphicsWindow.cxx b/panda/src/webgldisplay/webGLGraphicsWindow.cxx index dc6d8e6f97..da094d1cb5 100644 --- a/panda/src/webgldisplay/webGLGraphicsWindow.cxx +++ b/panda/src/webgldisplay/webGLGraphicsWindow.cxx @@ -20,6 +20,13 @@ #include "pointerData.h" #include +#ifndef CPPPARSER +extern "C" void EMSCRIPTEN_KEEPALIVE +_canvas_resized(WebGLGraphicsWindow *window, double width, double height) { + window->on_resize(width, height); +} +#endif + TypeHandle WebGLGraphicsWindow::_type_handle; /** @@ -171,8 +178,9 @@ set_properties_now(WindowProperties &properties) { const char *target = "#canvas"; if (properties.has_size()) { - emscripten_set_canvas_element_size(target, properties.get_x_size(), properties.get_y_size()); _properties.set_size(properties.get_size()); + emscripten_set_canvas_element_size(target, properties.get_x_size(), properties.get_y_size()); + emscripten_set_element_css_size(target, properties.get_x_size(), properties.get_y_size()); properties.clear_size(); set_size_and_recalc(_properties.get_x_size(), _properties.get_y_size()); throw_event(get_window_event(), this); @@ -260,6 +268,14 @@ close_window() { _gsg.clear(); } + EM_ASM({ + var canvas = document.getElementById('canvas'); + if (canvas && canvas._p3d_resizeObserver) { + canvas._p3d_resizeObserver.disconnect(); + delete canvas._p3d_resizeObserver; + } + }); + // Clear the assigned callbacks. const char *target = "#canvas"; emscripten_set_fullscreenchange_callback(target, nullptr, false, nullptr); @@ -317,11 +333,22 @@ open_window() { return false; } - if (_properties.has_size() && _properties.get_size() != LVecBase2i(1, 1)) { + // For now, always use the size specified in the CSS, except when fixed size + // has been specified, or the CSS has no size + double css_width, css_height; + emscripten_get_element_css_size(target, &css_width, &css_height); + + if (_properties.has_size() && (_properties.get_fixed_size() || css_width == 0.0 || css_height == 0.0)) { emscripten_set_canvas_element_size(target, _properties.get_x_size(), _properties.get_y_size()); + emscripten_set_element_css_size(target, _properties.get_x_size(), _properties.get_y_size()); } else { - int width, height; - emscripten_get_canvas_element_size(target, &width, &height); + int width = (int)css_width; + int height = (int)css_height; + if (width == 0 || height == 0) { + emscripten_get_canvas_element_size(target, &width, &height); + } else { + emscripten_set_canvas_element_size(target, width, height); + } _properties.set_size(width, height); EmscriptenFullscreenChangeEvent event; @@ -379,6 +406,29 @@ open_window() { emscripten_set_wheel_callback(target, user_data, false, &on_wheel_event); + // Emscripten has no working resize handler for the canvas element, we'll + // have to create our own + EM_ASM({ + var canvas = document.getElementById('canvas'); + if (canvas) { + var observer = new ResizeObserver(function(entries) { + var entry = entries[0]; + if (entry) { + var width = entry.contentRect.width; + var height = entry.contentRect.height; + if (width != 0 && height != 0) { + __canvas_resized($0, entry.contentRect.width, entry.contentRect.height); + } + } + }); + observer.observe(canvas); + if (canvas._p3d_resizeObserver) { + canvas._p3d_resizeObserver.disconnect(); + } + canvas._p3d_resizeObserver = observer; + } + }, this); + if (!_properties.has_foreground() || _properties.get_foreground()) { _properties.set_foreground(EM_ASM_INT({ var canvas = document.getElementById('canvas'); @@ -395,6 +445,24 @@ open_window() { return true; } +/** + * + */ +void WebGLGraphicsWindow:: +on_resize(double width, double height) { + if (_properties.get_fixed_size()) { + return; + } + + const char *target = "#canvas"; + + LVecBase2i size((int)width, (int)height); + emscripten_set_canvas_element_size(target, size[0], size[1]); + if (_properties.get_size() != size) { + system_changed_properties(WindowProperties::size(size)); + } +} + /** * */ diff --git a/panda/src/webgldisplay/webGLGraphicsWindow.h b/panda/src/webgldisplay/webGLGraphicsWindow.h index 16535cc730..3b905f8543 100644 --- a/panda/src/webgldisplay/webGLGraphicsWindow.h +++ b/panda/src/webgldisplay/webGLGraphicsWindow.h @@ -48,6 +48,9 @@ protected: virtual void close_window(); virtual bool open_window(); +public: + void on_resize(double width, double height); + private: static EM_BOOL on_fullscreen_event(int type, const EmscriptenFullscreenChangeEvent *event, void *user_data); static EM_BOOL on_pointerlock_event(int type, const EmscriptenPointerlockChangeEvent *event, void *user_data); From 9a2203b9bbf7e8c0da802c2317eca3fdd4a3af36 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 25 Feb 2025 17:32:44 +0100 Subject: [PATCH 064/291] pstats: Backport fix for crash when receiving frames out of order --- pandatool/src/pstatserver/pStatThreadData.cxx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pandatool/src/pstatserver/pStatThreadData.cxx b/pandatool/src/pstatserver/pStatThreadData.cxx index bef18c8a24..1df4e7c0e7 100644 --- a/pandatool/src/pstatserver/pStatThreadData.cxx +++ b/pandatool/src/pstatserver/pStatThreadData.cxx @@ -290,6 +290,14 @@ record_new_frame(int frame_number, PStatFrameData *frame_data) { } int index = frame_number - _first_frame_number; + + // It's possible to receive frames out of order. + while (index < 0) { + _frames.push_front(nullptr); + ++index; + --_first_frame_number; + } + nassertv(index >= 0 && index < (int)_frames.size()); if (_frames[index] != nullptr) { From fcf619d6067ba6d8e072b43a3ec9a4d1e1fc8f2d Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 25 Feb 2025 17:33:34 +0100 Subject: [PATCH 065/291] Update version numbers to 1.10.16 --- dtool/PandaVersion.pp | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dtool/PandaVersion.pp b/dtool/PandaVersion.pp index c3914608fd..e7f75274e7 100644 --- a/dtool/PandaVersion.pp +++ b/dtool/PandaVersion.pp @@ -7,7 +7,7 @@ // place to put this. // Use spaces to separate the major, minor, and sequence numbers here. -#define PANDA_VERSION 1 10 15 +#define PANDA_VERSION 1 10 16 // This variable will be defined to false in the CVS repository, but // scripts that generate source tarballs and/or binary releases for diff --git a/setup.cfg b/setup.cfg index c252d8d575..e8cf51c227 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = Panda3D -version = 1.10.15 +version = 1.10.16 url = https://www.panda3d.org/ description = Panda3D is a framework for 3D rendering and game development for Python and C++ programs. license = Modified BSD License From 5a97488adfcbab5b9ebae37cf763eb5a43cd0cc4 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 27 Feb 2025 15:10:53 +0100 Subject: [PATCH 066/291] gtk-stats: Support trackpad zoom/pan gestures in Timeline view --- pandatool/src/gtk-stats/gtkStatsTimeline.cxx | 33 +++++++++++++++++++- pandatool/src/gtk-stats/gtkStatsTimeline.h | 4 +++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pandatool/src/gtk-stats/gtkStatsTimeline.cxx b/pandatool/src/gtk-stats/gtkStatsTimeline.cxx index 5010b2dabc..bffdd32e57 100644 --- a/pandatool/src/gtk-stats/gtkStatsTimeline.cxx +++ b/pandatool/src/gtk-stats/gtkStatsTimeline.cxx @@ -54,7 +54,8 @@ GtkStatsTimeline(GtkStatsMonitor *monitor) : G_CALLBACK(thread_area_draw_callback), this); // Listen for mouse wheel and keyboard events. - gtk_widget_add_events(_graph_window, GDK_SCROLL_MASK | + gtk_widget_add_events(_graph_window, GDK_SMOOTH_SCROLL_MASK | + GDK_SCROLL_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); gtk_widget_set_can_focus(_graph_window, TRUE); @@ -65,6 +66,25 @@ GtkStatsTimeline(GtkStatsMonitor *monitor) : g_signal_connect(G_OBJECT(_graph_window), "key_release_event", G_CALLBACK(key_release_callback), this); + // Set up trackpad pinch and swipe gestures. + _zoom_gesture = gtk_gesture_zoom_new(_graph_window); + g_signal_connect(_zoom_gesture, "begin", + G_CALLBACK(+[](GtkGestureZoom *gesture, GdkEventSequence *sequence, gpointer data) { + GtkStatsTimeline *self = (GtkStatsTimeline *)data; + self->_zoom_scale = self->get_horizontal_scale(); + }), this); + + g_signal_connect(_zoom_gesture, "scale-changed", + G_CALLBACK((+[](GtkGestureZoom *gesture, gdouble scale, gpointer data) { + GtkStatsTimeline *self = (GtkStatsTimeline *)data; + gdouble x, y; + if (gtk_gesture_get_point(GTK_GESTURE(gesture), NULL, &x, &y)) { + int graph_x = (int)(x * self->_cr_scale); + self->zoom_by(log(scale) * 0.8, self->pixel_to_timestamp(graph_x)); + self->start_animation(); + } + })), this); + int min_height = 0; if (!_threads.empty()) { double height = row_to_pixel(get_num_rows()) + _pixel_scale * 2.5; @@ -100,6 +120,7 @@ GtkStatsTimeline(GtkStatsMonitor *monitor) : GtkStatsTimeline:: ~GtkStatsTimeline() { cairo_pattern_destroy(_grid_pattern); + g_object_unref(_zoom_gesture); } /** @@ -627,6 +648,16 @@ handle_scroll(int graph_x, int graph_y, double dx, double dy, bool ctrl_held) { return handled; } +/** + * + */ +gboolean GtkStatsTimeline:: +handle_zoom(int graph_x, int graph_y, double scale) { + zoom_to(get_horizontal_scale() / scale, pixel_to_timestamp(graph_x)); + start_animation(); + return TRUE; +} + /** * */ diff --git a/pandatool/src/gtk-stats/gtkStatsTimeline.h b/pandatool/src/gtk-stats/gtkStatsTimeline.h index 37f3f38167..b354712d66 100644 --- a/pandatool/src/gtk-stats/gtkStatsTimeline.h +++ b/pandatool/src/gtk-stats/gtkStatsTimeline.h @@ -63,6 +63,7 @@ protected: virtual gboolean handle_leave(); gboolean handle_scroll(int graph_x, int graph_y, double dx, double dy, bool ctrl_held); + gboolean handle_zoom(int graph_x, int graph_y, double scale); gboolean handle_key(bool pressed, guint val, guint16 hw_code); private: @@ -92,6 +93,9 @@ private: int _highlighted_x = 0; int _scroll = 0; ColorBar _popup_bar; + + double _zoom_scale = 1.0; + GtkGesture *_zoom_gesture = nullptr; }; #endif From 6274b478aad8bac262471c361e677d45804a2295 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 27 Feb 2025 15:11:16 +0100 Subject: [PATCH 067/291] pstats: Better handle overlapping collectors in Timeline Prevents staircase effect where overlapping bars will end up ever increasing the number of rows --- pandatool/src/pstatserver/pStatTimeline.cxx | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pandatool/src/pstatserver/pStatTimeline.cxx b/pandatool/src/pstatserver/pStatTimeline.cxx index 01f9020f37..d8d0fdfbc2 100644 --- a/pandatool/src/pstatserver/pStatTimeline.cxx +++ b/pandatool/src/pstatserver/pStatTimeline.cxx @@ -196,6 +196,7 @@ update_bars(int thread_index, int frame_number) { // pair pvector > stack; + bool had_children = false; double frame_start = frame_data.get_start() + _clock_skew; double prev = frame_start; @@ -275,6 +276,7 @@ update_bars(int thread_index, int frame_number) { thread_row._rows.resize(stack.size()); changed_num_rows = true; } + had_children = false; } } else if (!stack.empty() && stack.back().first == collector_index) { @@ -288,6 +290,7 @@ update_bars(int thread_index, int frame_number) { while (!stack.empty() && stack.back().first < 0) { stack.pop_back(); } + had_children = true; } else { // Unlikely case: ending a collector before a "child" has ended. @@ -349,6 +352,33 @@ update_bars(int thread_index, int frame_number) { frame_start, time, collector_index, thread_index, frame_number, true, false}); } + else if (i > 0 && !had_children) { + // Figure out if the currently active collector could actually be + // slotted higher. This prevents the staircase effect where + // overlapping collectors will cause the number of rows to continue + // to grow at every overlap. We only do this if the current + // collector has not had children yet, that'd be too confusing. + int j = stack.size() - 3; + while (j > 0) { + auto &item = stack[j]; + if (item.first >= 0) { + // Nope, can't do. + break; + } + // Yes, does this row have enough space? + Row &row = thread_row._rows[j]; + if (row.empty() || row.back()._end < stack.back().second) { + // It does. + item = std::move(stack.back()); + stack.pop_back(); + while (!stack.empty() && stack.back().first < 0) { + stack.pop_back(); + } + break; + } + --j; + } + } } } From 4b4fa038955d56710033a5022d40bfee9992318c Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Sun, 6 Apr 2025 02:10:15 -0600 Subject: [PATCH 068/291] direct: Annotate functions called directly by top-level code (#1727) --- direct/src/distributed/ClockDelta.py | 2 +- .../extension_native_helpers.py | 10 +++-- direct/src/showbase/PythonUtil.py | 41 ++++++++++++------- direct/src/showbase/ShowBase.py | 2 +- direct/src/stdpy/thread.py | 2 +- direct/src/stdpy/threading2.py | 2 +- direct/src/tkpanels/Inspector.py | 2 +- 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/direct/src/distributed/ClockDelta.py b/direct/src/distributed/ClockDelta.py index 7f8da75706..ebba8a7c97 100644 --- a/direct/src/distributed/ClockDelta.py +++ b/direct/src/distributed/ClockDelta.py @@ -48,7 +48,7 @@ class ClockDelta(DirectObject.DirectObject): notify = DirectNotifyGlobal.directNotify.newCategory('ClockDelta') - def __init__(self): + def __init__(self) -> None: self.globalClock = ClockObject.getGlobalClock() # self.delta is the relative delta from our clock to the diff --git a/direct/src/extensions_native/extension_native_helpers.py b/direct/src/extensions_native/extension_native_helpers.py index 2a302f415e..59176d6851 100644 --- a/direct/src/extensions_native/extension_native_helpers.py +++ b/direct/src/extensions_native/extension_native_helpers.py @@ -1,15 +1,19 @@ +from __future__ import annotations + __all__ = ["Dtool_ObjectToDict", "Dtool_funcToMethod"] +from collections.abc import Callable + def Dtool_ObjectToDict(cls, name, obj): cls.DtoolClassDict[name] = obj -def Dtool_funcToMethod(func, cls, method_name=None): +def Dtool_funcToMethod(func: Callable, cls, method_name: str | None = None) -> None: """Adds func to class so it is an accessible method; use method_name to specify the name to be used for calling the method. The new method is accessible to any instance immediately.""" - func.__func__ = func - func.__self__ = None + func.__func__ = func # type: ignore[attr-defined] + func.__self__ = None # type: ignore[attr-defined] if not method_name: method_name = func.__name__ cls.DtoolClassDict[method_name] = func diff --git a/direct/src/showbase/PythonUtil.py b/direct/src/showbase/PythonUtil.py index d9ffa2aa35..60a19a7700 100644 --- a/direct/src/showbase/PythonUtil.py +++ b/direct/src/showbase/PythonUtil.py @@ -41,13 +41,17 @@ import time import builtins import importlib import functools -from typing import Callable +from collections.abc import Callable, Container, Iterable, Mapping +from typing import Any, Generic, TypeVar __report_indent = 3 from panda3d.core import ConfigVariableBool, ConfigVariableString, ConfigFlags from panda3d.core import ClockObject +_T = TypeVar('_T') +_KT = TypeVar('_KT') +_VT = TypeVar('_VT') ## with one integer positional arg, this uses about 4/5 of the memory of the Functor class below #def Functor(function, *args, **kArgs): @@ -100,9 +104,9 @@ class Functor: return s -class Stack: - def __init__(self): - self.__list = [] +class Stack(Generic[_T]): + def __init__(self) -> None: + self.__list: list[_T] = [] def push(self, item): self.__list.append(item) @@ -411,7 +415,7 @@ def list2dict(L, value=None): return dict([(k, value) for k in L]) -def listToIndex2item(L): +def listToIndex2item(L: Iterable[_VT]) -> dict[int, _VT]: """converts list to dict of list index->list item""" d = {} for i, item in enumerate(L): @@ -422,7 +426,7 @@ def listToIndex2item(L): assert listToIndex2item(['a','b']) == {0: 'a', 1: 'b',} -def listToItem2index(L): +def listToItem2index(L: Iterable[_KT]) -> dict[_KT, int]: """converts list to dict of list item->list index This is lossy if there are duplicate list items""" d = {} @@ -451,7 +455,7 @@ def invertDict(D, lossy=False): return n -def invertDictLossless(D): +def invertDictLossless(D: Mapping[_KT, _VT]) -> dict[_VT, list[_KT]]: """similar to invertDict, but values of new dict are lists of keys from old dict. No information is lost. @@ -459,7 +463,7 @@ def invertDictLossless(D): >>> invertDictLossless(old) {1: ['key1'], 2: ['key2', 'keyA']} """ - n = {} + n: dict[_VT, list[_KT]] = {} for key, value in D.items(): n.setdefault(value, []) n[value].append(key) @@ -706,7 +710,7 @@ if __debug__: movedDumpFuncs: list[Callable] = [] movedLoadFuncs: list[Callable] = [] profileFilenames = set() - profileFilenameList = Stack() + profileFilenameList = Stack[str]() profileFilename2file = {} profileFilename2marshalData = {} @@ -1278,7 +1282,7 @@ def randInt32(rng=random.random): class SerialNumGen: """generates serial numbers""" - def __init__(self, start=None): + def __init__(self, start: int | None = None) -> None: if start is None: start = 0 self.__counter = start-1 @@ -1305,7 +1309,7 @@ class SerialMaskedGen(SerialNumGen): _serialGen = SerialNumGen() -def serialNum(): +def serialNum() -> int: return _serialGen.next() @@ -1588,7 +1592,7 @@ class ScratchPad: class Sync: _SeriesGen = SerialNumGen() - def __init__(self, name, other=None): + def __init__(self, name: str, other: Sync | None = None) -> None: self._name = name if other is None: self._series = self._SeriesGen.next() @@ -1972,7 +1976,13 @@ def pstatcollect(scope, level = None): __report_indent = 0 -def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigParam = []): +def report( + types: Container[str] = [], + prefix: str = '', + xform: Callable[[Any], object] | None = None, + notifyFunc: Callable[[str], object] | None = None, + dConfigParam: str | list[str] | tuple[str, ...] = [], +) -> Callable[[_T], _T]: """ This is a decorator generating function. Use is similar to a @decorator, except you must be sure to call it as a function. @@ -2031,7 +2041,7 @@ def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigPara return f try: - if not __dev__ and not ConfigVariableBool('force-reports', False): + if not __dev__ and not ConfigVariableBool('force-reports', False): # type: ignore[name-defined] return decorator # determine whether we should use the decorator @@ -2041,6 +2051,7 @@ def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigPara if not dConfigParam: doPrint = True else: + dConfigParams: list[str] | tuple[str, ...] if not isinstance(dConfigParam, (list,tuple)): dConfigParams = (dConfigParam,) else: @@ -2070,7 +2081,7 @@ def report(types = [], prefix = '', xform = None, notifyFunc = None, dConfigPara globalClockDelta = importlib.import_module("direct.distributed.ClockDelta").globalClockDelta - def decorator(f): + def decorator(f): # type: ignore[no-redef] def wrap(*args, **kwargs): if args: rArgs = [args[0].__class__.__name__ + ', '] diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 26370c65a1..ce0de27579 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -382,7 +382,7 @@ class ShowBase(DirectObject.DirectObject): #: yourself every frame. self.cTrav: CollisionTraverser | Literal[0] = 0 self.shadowTrav: CollisionTraverser | Literal[0] = 0 - self.cTravStack = Stack() + self.cTravStack = Stack[CollisionTraverser]() # Ditto for an AppTraverser. self.appTrav: Any | Literal[0] = 0 diff --git a/direct/src/stdpy/thread.py b/direct/src/stdpy/thread.py index f20a2a54bc..69240721bb 100644 --- a/direct/src/stdpy/thread.py +++ b/direct/src/stdpy/thread.py @@ -250,7 +250,7 @@ def exit(): raise SystemExit -def allocate_lock(): +def allocate_lock() -> LockType: return LockType() diff --git a/direct/src/stdpy/threading2.py b/direct/src/stdpy/threading2.py index 09d0a8d083..b2d49cdc90 100644 --- a/direct/src/stdpy/threading2.py +++ b/direct/src/stdpy/threading2.py @@ -654,7 +654,7 @@ class _Timer(Thread): class _MainThread(Thread): - def __init__(self): + def __init__(self) -> None: Thread.__init__(self, name="MainThread") self._Thread__started = True _active_limbo_lock.acquire() diff --git a/direct/src/tkpanels/Inspector.py b/direct/src/tkpanels/Inspector.py index 51e448f30d..5dbf79c1f9 100644 --- a/direct/src/tkpanels/Inspector.py +++ b/direct/src/tkpanels/Inspector.py @@ -48,7 +48,7 @@ def inspectorFor(anObject): ### initializing -def initializeInspectorMap(): +def initializeInspectorMap() -> None: global _InspectorMap notFinishedTypes = ['BufferType', 'EllipsisType', 'FrameType', 'TracebackType', 'XRangeType'] From 54355795673880cadb3b006becc35f87630bf083 Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Sun, 6 Apr 2025 02:10:50 -0600 Subject: [PATCH 069/291] showbase: Annotate basic `Messenger` methods (#1726) --- direct/src/showbase/DirectObject.py | 19 ++++++-- direct/src/showbase/Messenger.py | 72 +++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/direct/src/showbase/DirectObject.py b/direct/src/showbase/DirectObject.py index 8365d9f94f..bdbae6fc3f 100644 --- a/direct/src/showbase/DirectObject.py +++ b/direct/src/showbase/DirectObject.py @@ -1,8 +1,14 @@ """Defines the DirectObject class, a convenient class to inherit from if the object needs to be able to respond to events.""" +from __future__ import annotations + __all__ = ['DirectObject'] +from typing import Callable + +from panda3d.core import AsyncTask + from direct.directnotify.DirectNotifyGlobal import directNotify from direct.task.TaskManagerGlobal import taskMgr from .MessengerGlobal import messenger @@ -12,6 +18,9 @@ class DirectObject: """ This is the class that all Direct/SAL classes should inherit from """ + _MSGRmessengerId: tuple[str, int] + _taskList: dict[int, AsyncTask] + #def __del__(self): # This next line is useful for debugging leaks #print "Destructing: ", self.__class__.__name__ @@ -19,16 +28,16 @@ class DirectObject: # Wrapper functions to have a cleaner, more object oriented approach to # the messenger functionality. - def accept(self, event, method, extraArgs=[]): - return messenger.accept(event, self, method, extraArgs, 1) + def accept(self, event: str, method: Callable, extraArgs: list = []) -> None: + return messenger.accept(event, self, method, extraArgs, True) def acceptOnce(self, event, method, extraArgs=[]): return messenger.accept(event, self, method, extraArgs, 0) - def ignore(self, event): + def ignore(self, event: str) -> None: return messenger.ignore(event, self) - def ignoreAll(self): + def ignoreAll(self) -> None: return messenger.ignoreAll(self) def isAccepting(self, event): @@ -41,7 +50,7 @@ class DirectObject: return messenger.isIgnoring(event, self) #This function must be used if you want a managed task - def addTask(self, *args, **kwargs): + def addTask(self, *args, **kwargs) -> AsyncTask: if not hasattr(self, "_taskList"): self._taskList = {} kwargs['owner'] = self diff --git a/direct/src/showbase/Messenger.py b/direct/src/showbase/Messenger.py index e6ef2153c2..cac8fbd754 100644 --- a/direct/src/showbase/Messenger.py +++ b/direct/src/showbase/Messenger.py @@ -2,19 +2,40 @@ :ref:`event handling ` that happens on the Python side. """ +from __future__ import annotations + __all__ = ['Messenger'] +import types +from collections.abc import Callable +from typing import Protocol +# These can be replaced with their builtin counterparts once support for Python 3.8 is dropped. +from typing import Dict, Tuple + +from panda3d.core import AsyncTask + from direct.stdpy.threading import Lock from direct.directnotify import DirectNotifyGlobal from .PythonUtil import safeRepr -import types + +# The following variables are typing constructs used in annotations +# to succinctly express complex type structures. +_ObjMsgrId = Tuple[str, int] +_CallbackInfo = list # [Callable, list, bool] +_ListenerObject = list # [int, DirectObject] +_AcceptorDict = Dict[_ObjMsgrId, _CallbackInfo] +_EventTuple = Tuple[_AcceptorDict, str, list, bool] + + +class _HasMessengerID(Protocol): + _MSGRmessengerId: _ObjMsgrId class Messenger: notify = DirectNotifyGlobal.directNotify.newCategory("Messenger") - def __init__(self): + def __init__(self) -> None: """ One is keyed off the event name. It has the following structure:: @@ -34,16 +55,16 @@ class Messenger: {'mouseDown': {avatar: [avatar.jump, [2.0], 1]}} """ # eventName->objMsgrId->callbackInfo - self.__callbacks = {} + self.__callbacks: dict[str, _AcceptorDict] = {} # objMsgrId->set(eventName) - self.__objectEvents = {} + self.__objectEvents: dict[_ObjMsgrId, dict[str, None]] = {} self._messengerIdGen = 0 # objMsgrId->listenerObject - self._id2object = {} + self._id2object: dict[_ObjMsgrId, _ListenerObject] = {} # A mapping of taskChain -> eventList, used for sending events # across task chains (and therefore across threads). - self._eventQueuesByTaskChain = {} + self._eventQueuesByTaskChain: dict[str, list[_EventTuple]] = {} # This protects the data structures within this object from # multithreaded access. @@ -51,7 +72,7 @@ class Messenger: if __debug__: self.__isWatching=0 - self.__watching={} + self.__watching: dict[str, bool] = {} # I'd like this to be in the __debug__, but I fear that someone will # want this in a release build. If you're sure that that will not be # then please remove this comment and put the quiet/verbose stuff @@ -62,7 +83,7 @@ class Messenger: 'collisionLoopFinished':1, } # see def quiet() - def _getMessengerId(self, object): + def _getMessengerId(self, object: _HasMessengerID) -> _ObjMsgrId: # TODO: allocate this id in DirectObject.__init__ and get derived # classes to call down (speed optimization, assuming objects # accept/ignore more than once over their lifetime) @@ -73,7 +94,7 @@ class Messenger: self._messengerIdGen += 1 return object._MSGRmessengerId - def _storeObject(self, object): + def _storeObject(self, object: _HasMessengerID) -> None: # store reference-counted reference to object in case we need to # retrieve it later. assumes lock is held. id = self._getMessengerId(object) @@ -82,7 +103,7 @@ class Messenger: else: self._id2object[id][0] += 1 - def _getObject(self, id): + def _getObject(self, id: _ObjMsgrId) -> _HasMessengerID: return self._id2object[id][1] def _getObjects(self): @@ -101,7 +122,7 @@ class Messenger: def _getEvents(self): return list(self.__callbacks.keys()) - def _releaseObject(self, object): + def _releaseObject(self, object: _HasMessengerID) -> None: # assumes lock is held. id = self._getMessengerId(object) if id in self._id2object: @@ -117,7 +138,14 @@ class Messenger: from .EventManagerGlobal import eventMgr return eventMgr.eventHandler.get_future(event) - def accept(self, event, object, method, extraArgs=[], persistent=1): + def accept( + self, + event: str, + object: _HasMessengerID, + method: Callable, + extraArgs: list = [], + persistent: bool = True, + ) -> None: """ accept(self, string, DirectObject, Function, List, Boolean) Make this object accept this event. When the event is @@ -174,7 +202,7 @@ class Messenger: finally: self.lock.release() - def ignore(self, event, object): + def ignore(self, event: str, object: _HasMessengerID) -> None: """ ignore(self, string, DirectObject) Make this object no longer respond to this event. It is safe to call even if it was not already accepting @@ -208,7 +236,7 @@ class Messenger: finally: self.lock.release() - def ignoreAll(self, object): + def ignoreAll(self, object: _HasMessengerID) -> None: """ Make this object no longer respond to any events it was accepting Useful for cleanup @@ -283,7 +311,7 @@ class Messenger: """ return not self.isAccepting(event, object) - def send(self, event, sentArgs=[], taskChain=None): + def send(self, event: str, sentArgs: list = [], taskChain: str | None = None) -> None: """ Send this event, optionally passing in arguments. @@ -305,12 +333,12 @@ class Messenger: self.lock.acquire() try: - foundWatch = 0 + foundWatch = False if __debug__: if self.__isWatching: for i in self.__watching: if str(event).find(i) >= 0: - foundWatch = 1 + foundWatch = True break acceptorDict = self.__callbacks.get(event) if not acceptorDict: @@ -336,7 +364,7 @@ class Messenger: finally: self.lock.release() - def __taskChainDispatch(self, taskChain, task): + def __taskChainDispatch(self, taskChain: str, task: AsyncTask) -> int: """ This task is spawned each time an event is sent across task chains. Its job is to empty the task events on the queue for this particular task chain. This guarantees that events @@ -365,7 +393,13 @@ class Messenger: return task.done - def __dispatch(self, acceptorDict, event, sentArgs, foundWatch): + def __dispatch( + self, + acceptorDict: _AcceptorDict, + event: str, + sentArgs: list, + foundWatch: bool, + ) -> None: for id in list(acceptorDict.keys()): # We have to make this apparently redundant check, because # it is possible that one object removes its own hooks From eae39b339ca3ba5e0271f353927cd02a433fe66b Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 15 Apr 2025 10:44:33 +0200 Subject: [PATCH 070/291] collide: Fix CollisionNode owner crash (grab GIL when destructing) --- panda/src/collide/collisionNode_ext.cxx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/panda/src/collide/collisionNode_ext.cxx b/panda/src/collide/collisionNode_ext.cxx index 8508cf5206..62e7d5f874 100644 --- a/panda/src/collide/collisionNode_ext.cxx +++ b/panda/src/collide/collisionNode_ext.cxx @@ -53,7 +53,17 @@ void Extension:: set_owner(PyObject *owner) { if (owner != Py_None) { PyObject *ref = PyWeakref_NewRef(owner, nullptr); - _this->set_owner(ref, [](void *obj) { Py_DECREF((PyObject *)obj); }); + _this->set_owner(ref, [](void *obj) { +#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS) + // Use PyGILState to protect this asynchronous call. + PyGILState_STATE gstate; + gstate = PyGILState_Ensure(); +#endif + Py_DECREF((PyObject *)obj); +#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS) + PyGILState_Release(gstate); +#endif + }); } else { _this->clear_owner(); } From 33e6ed890792d202e80bf3ddf11c80db0fb46061 Mon Sep 17 00:00:00 2001 From: omn14 Date: Tue, 15 Apr 2025 15:59:15 +0200 Subject: [PATCH 071/291] physics: Add support for distance-based falloff (#1731) --- direct/src/tkpanels/ParticlePanel.py | 17 ++++++++++++++++- panda/src/physics/linearDistanceForce.h | 5 ++++- panda/src/physics/linearSinkForce.cxx | 20 +++++++++++++++++++- panda/src/physics/linearSourceForce.cxx | 20 +++++++++++++++++++- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/direct/src/tkpanels/ParticlePanel.py b/direct/src/tkpanels/ParticlePanel.py index 6d41ba808f..f64cbad8c9 100644 --- a/direct/src/tkpanels/ParticlePanel.py +++ b/direct/src/tkpanels/ParticlePanel.py @@ -2872,6 +2872,12 @@ class ParticlePanel(AppShell): if type == 'FT_ONE_OVER_R_CUBED': #f.setFalloffType(LinearDistanceForce.FTONEOVERRCUBED) f.setFalloffType(2) + if type == 'FT_ONE_OVER_R_OVER_DISTANCE': + f.setFalloffType(3) + if type == 'FT_ONE_OVER_R_OVER_DISTANCE_SQUARED': + f.setFalloffType(4) + if type == 'FT_ONE_OVER_R_OVER_DISTANCE_CUBED': + f.setFalloffType(5) def setForceCenter(vec, f = force): f.setForceCenter(Point3(vec[0], vec[1], vec[2])) @@ -2886,7 +2892,10 @@ class ParticlePanel(AppShell): 'Set force falloff type', ('FT_ONE_OVER_R', 'FT_ONE_OVER_R_SQUARED', - 'FT_ONE_OVER_R_CUBED'), + 'FT_ONE_OVER_R_CUBED', + 'FT_ONE_OVER_R_OVER_DISTANCE', + 'FT_ONE_OVER_R_OVER_DISTANCE_SQUARED', + 'FT_ONE_OVER_R_OVER_DISTANCE_CUBED'), command = setFalloffType) self.getWidget(pageName, forceName + ' Falloff').configure( label_width = 16) @@ -2897,6 +2906,12 @@ class ParticlePanel(AppShell): var.set('FT_ONE_OVER_R_SQUARED') elif falloff == LinearDistanceForce.FTONEOVERRCUBED: var.set('FT_ONE_OVER_R_CUBED') + elif falloff == LinearDistanceForce.FTONEOVERROVERDISTANCE: + var.set('FT_ONE_OVER_R_OVER_DISTANCE') + elif falloff == LinearDistanceForce.FTONEOVERROVERDISTANCESQUARED: + var.set('FT_ONE_OVER_R_OVER_DISTANCE_SQUARED') + elif falloff == LinearDistanceForce.FTONEOVERROVERDISTANCECUBED: + var.set('FT_ONE_OVER_R_OVER_DISTANCE_CUBED') vec = force.getForceCenter() self.createVector3Entry(frame, pageName, forceName + ' Center', 'Set center of force', diff --git a/panda/src/physics/linearDistanceForce.h b/panda/src/physics/linearDistanceForce.h index fa245db104..6de191f052 100644 --- a/panda/src/physics/linearDistanceForce.h +++ b/panda/src/physics/linearDistanceForce.h @@ -26,7 +26,10 @@ PUBLISHED: enum FalloffType { FT_ONE_OVER_R, FT_ONE_OVER_R_SQUARED, - FT_ONE_OVER_R_CUBED + FT_ONE_OVER_R_CUBED, + FT_ONE_OVER_R_OVER_DISTANCE, + FT_ONE_OVER_R_OVER_DISTANCE_SQUARED, + FT_ONE_OVER_R_OVER_DISTANCE_CUBED }; INLINE void set_radius(PN_stdfloat r); diff --git a/panda/src/physics/linearSinkForce.cxx b/panda/src/physics/linearSinkForce.cxx index ab1268cd5c..52d5e04174 100644 --- a/panda/src/physics/linearSinkForce.cxx +++ b/panda/src/physics/linearSinkForce.cxx @@ -61,7 +61,25 @@ make_copy() { */ LVector3 LinearSinkForce:: get_child_vector(const PhysicsObject *po) { - return (get_force_center() - po->get_position()) * get_scalar_term(); + LVector3 distance_vector = get_force_center() - po->get_position(); + PN_stdfloat distance_squared = distance_vector.length_squared(); + + if (distance_squared == 0) { + return distance_vector; + } + + PN_stdfloat scalar = get_scalar_term(); + + switch (get_falloff_type()) { + case FT_ONE_OVER_R_OVER_DISTANCE: + return (distance_vector / sqrt(distance_squared)) * scalar; + case FT_ONE_OVER_R_OVER_DISTANCE_SQUARED: + return (distance_vector / distance_squared) * scalar; + case FT_ONE_OVER_R_OVER_DISTANCE_CUBED: + return (distance_vector / (distance_squared * sqrt(distance_squared))) * scalar; + default: + return distance_vector * scalar; + } } /** diff --git a/panda/src/physics/linearSourceForce.cxx b/panda/src/physics/linearSourceForce.cxx index 4bb674dbad..db21b4b1f2 100644 --- a/panda/src/physics/linearSourceForce.cxx +++ b/panda/src/physics/linearSourceForce.cxx @@ -61,7 +61,25 @@ make_copy() { */ LVector3 LinearSourceForce:: get_child_vector(const PhysicsObject *po) { - return (po->get_position() - get_force_center()) * get_scalar_term(); + LVector3 distance_vector = po->get_position() - get_force_center(); + PN_stdfloat distance_squared = distance_vector.length_squared(); + + if (distance_squared == 0) { + return distance_vector; + } + + PN_stdfloat scalar = get_scalar_term(); + + switch (get_falloff_type()) { + case FT_ONE_OVER_R_OVER_DISTANCE: + return (distance_vector / sqrt(distance_squared)) * scalar; + case FT_ONE_OVER_R_OVER_DISTANCE_SQUARED: + return (distance_vector / distance_squared) * scalar; + case FT_ONE_OVER_R_OVER_DISTANCE_CUBED: + return (distance_vector / (distance_squared * sqrt(distance_squared))) * scalar; + default: + return distance_vector * scalar; + } } /** From 3191a450abbfca19d60c9ffabc5e1b1d95d5cf22 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 15 Apr 2025 21:38:41 +0200 Subject: [PATCH 072/291] workflow: Update Ubuntu runners to 22.04 --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04cea3eec7..68ba2a7578 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: include: - profile: ubuntu-bionic-double-standard-unity-makefile - os: ubuntu-20.04 + os: ubuntu-22.04 config: Standard unity: YES generator: Unix Makefiles @@ -31,7 +31,7 @@ jobs: double: YES - profile: ubuntu-bionic-coverage-ninja - os: ubuntu-20.04 + os: ubuntu-22.04 config: Coverage unity: NO generator: Ninja @@ -372,12 +372,12 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: - os: [ubuntu-20.04, windows-2019, macOS-13] + os: [ubuntu-22.04, windows-2019, macOS-13] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Install dependencies (Ubuntu) - if: matrix.os == 'ubuntu-20.04' + if: matrix.os == 'ubuntu-22.04' run: | sudo apt-get update sudo apt-get install build-essential bison flex libfreetype6-dev libgl1-mesa-dev libjpeg-dev libode-dev libopenal-dev libpng-dev libssl-dev libvorbis-dev libx11-dev libxcursor-dev libxrandr-dev nvidia-cg-toolkit zlib1g-dev From 98a50a794d4d00c19660d29cfcddba7517514e46 Mon Sep 17 00:00:00 2001 From: omn14 Date: Wed, 16 Apr 2025 13:52:24 +0200 Subject: [PATCH 073/291] x11: Add function for detecting auto repeat key events (#1735) --- panda/src/x11display/config_x11display.cxx | 4 ++++ panda/src/x11display/config_x11display.h | 1 + panda/src/x11display/x11GraphicsWindow.cxx | 24 ++++++++++++++++++++-- panda/src/x11display/x11GraphicsWindow.h | 2 ++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/panda/src/x11display/config_x11display.cxx b/panda/src/x11display/config_x11display.cxx index 1744a8a556..6b8e03d27c 100644 --- a/panda/src/x11display/config_x11display.cxx +++ b/panda/src/x11display/config_x11display.cxx @@ -91,6 +91,10 @@ ConfigVariableBool x_send_startup_notification "lets the window manager know that an application has launched, so " "that it no longer needs to display a spinning mouse cursor.")); +ConfigVariableBool x_detectable_auto_repeat +("x-detectable-auto-repeat", false, + PRC_DESC("Set this true to enable detectable auto-repeat for keyboard input.")); + /** * Initializes the library. This must be called at least once before any of * the functions or classes in this library can be used. Normally it will be diff --git a/panda/src/x11display/config_x11display.h b/panda/src/x11display/config_x11display.h index d45ff1cd66..351f254640 100644 --- a/panda/src/x11display/config_x11display.h +++ b/panda/src/x11display/config_x11display.h @@ -37,5 +37,6 @@ extern ConfigVariableInt x_cursor_size; extern ConfigVariableString x_wm_class_name; extern ConfigVariableString x_wm_class; extern ConfigVariableBool x_send_startup_notification; +extern ConfigVariableBool x_detectable_auto_repeat; #endif diff --git a/panda/src/x11display/x11GraphicsWindow.cxx b/panda/src/x11display/x11GraphicsWindow.cxx index 49c1e19b13..b6b26194e7 100644 --- a/panda/src/x11display/x11GraphicsWindow.cxx +++ b/panda/src/x11display/x11GraphicsWindow.cxx @@ -97,8 +97,7 @@ x11GraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe, int flags, GraphicsStateGuardian *gsg, GraphicsOutput *host) : - GraphicsWindow(engine, pipe, name, fb_prop, win_prop, flags, gsg, host) -{ + GraphicsWindow(engine, pipe, name, fb_prop, win_prop, flags, gsg, host) { x11GraphicsPipe *x11_pipe; DCAST_INTO_V(x11_pipe, _pipe); _display = x11_pipe->get_display(); @@ -126,6 +125,8 @@ x11GraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe, PT(GraphicsWindowInputDevice) device = GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard_mouse"); add_input_device(device); _input = device; + + enable_detectable_auto_repeat(); } /** @@ -2745,3 +2746,22 @@ xim_preedit_done(XIC ic, XPointer client_data, XPointer call_data) { x11GraphicsWindow *window = (x11GraphicsWindow *)client_data; window->handle_preedit_done(); } + +/** + * Enables detectable auto-repeat if supported by the X server. + */ +void x11GraphicsWindow:: +enable_detectable_auto_repeat() { + if (!x_detectable_auto_repeat) { + return; + } + + Bool supported; + if (XkbSetDetectableAutoRepeat(_display, True, &supported)) { + x11display_cat.info() << "Detectable auto-repeat enabled.\n"; + } else if (!supported) { + x11display_cat.warning() << "Detectable auto-repeat is not supported by the X server.\n"; + } else { + x11display_cat.error() << "Failed to set detectable auto-repeat.\n"; + } +} diff --git a/panda/src/x11display/x11GraphicsWindow.h b/panda/src/x11display/x11GraphicsWindow.h index e147f907a5..4ed95c8a11 100644 --- a/panda/src/x11display/x11GraphicsWindow.h +++ b/panda/src/x11display/x11GraphicsWindow.h @@ -46,6 +46,8 @@ public: INLINE X11_Window get_xwindow() const; + void enable_detectable_auto_repeat(); + protected: virtual void close_window(); virtual bool open_window(); From cbb313d7329fd32ff85a517b50f048974aba00b5 Mon Sep 17 00:00:00 2001 From: Olivier Roussel Date: Sun, 11 May 2025 09:15:12 +0200 Subject: [PATCH 074/291] Use Win32 native calls for atomic also with clang-cl (#1746) --- dtool/src/dtoolbase/atomicAdjust.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtool/src/dtoolbase/atomicAdjust.h b/dtool/src/dtoolbase/atomicAdjust.h index 804224e5c3..014c4c4445 100644 --- a/dtool/src/dtoolbase/atomicAdjust.h +++ b/dtool/src/dtoolbase/atomicAdjust.h @@ -30,7 +30,7 @@ struct AtomicAdjust { #include "atomicAdjustDummyImpl.h" typedef AtomicAdjustDummyImpl AtomicAdjust; -#elif (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7))) || (defined(__clang__) && (__clang_major__ >= 3)) +#elif (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7))) || (defined(__clang__) && (__clang_major__ >= 3) && !defined(WIN32_VC) ) // GCC 4.7 and above has built-in __atomic functions for atomic operations. // Clang 3.0 and above also supports them. From 47a823b33080a7c7662aa38640ff5db6170aef8c Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 20 Jul 2025 21:46:33 +0200 Subject: [PATCH 075/291] workflow: Update Windows 2019 to 2022 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c12aac6ab..4353c98638 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: - os: [ubuntu-20.04, windows-2019, macOS-12] + os: [ubuntu-20.04, windows-2022, macOS-12] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 From 7051aa00c4994fcfd39cb4dc36505ace342dafb8 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 21 Jul 2025 11:59:41 +0200 Subject: [PATCH 076/291] workflow: Update test runners --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4353c98638..166b627189 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,12 +6,12 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: - os: [ubuntu-20.04, windows-2022, macOS-12] + os: [ubuntu-22.04, windows-2022, macOS-13] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 - name: Install dependencies (Ubuntu) - if: matrix.os == 'ubuntu-20.04' + if: matrix.os == 'ubuntu-22.04' run: | sudo apt-get update sudo apt-get install build-essential bison flex libfreetype6-dev libgl1-mesa-dev libjpeg-dev libode-dev libopenal-dev libpng-dev libssl-dev libvorbis-dev libx11-dev libxcursor-dev libxrandr-dev nvidia-cg-toolkit zlib1g-dev @@ -39,7 +39,7 @@ jobs: - name: Build Python 3.13 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 - name: Test Python 3.13 shell: bash run: | @@ -53,7 +53,7 @@ jobs: - name: Build Python 3.12 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 - name: Test Python 3.12 shell: bash run: | @@ -67,7 +67,7 @@ jobs: - name: Build Python 3.11 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 - name: Test Python 3.11 shell: bash run: | @@ -81,7 +81,7 @@ jobs: - name: Build Python 3.10 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 - name: Test Python 3.10 shell: bash run: | @@ -95,7 +95,7 @@ jobs: - name: Build Python 3.9 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 - name: Test Python 3.9 shell: bash run: | @@ -109,7 +109,7 @@ jobs: - name: Build Python 3.8 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 - name: Test Python 3.8 shell: bash run: | From e51c714aed1a41830b6b6631f8396ecea9d74344 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 21 Jul 2025 12:16:47 +0200 Subject: [PATCH 077/291] workflow: Update to MSVC 14.3 / 2022 --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68ba2a7578..f734840785 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -409,7 +409,7 @@ jobs: - name: Build Python 3.13 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.13 shell: bash run: | @@ -423,7 +423,7 @@ jobs: - name: Build Python 3.12 (double) shell: bash run: | - python makepanda/makepanda.py --override STDFLOAT_DOUBLE=1 --git-commit=${{github.sha}} --outputdir=built_dbl --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --override STDFLOAT_DOUBLE=1 --git-commit=${{github.sha}} --outputdir=built_dbl --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.12 (double) shell: bash run: | @@ -441,7 +441,7 @@ jobs: - name: Build Python 3.11 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.11 shell: bash run: | @@ -455,7 +455,7 @@ jobs: - name: Build Python 3.10 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.10 shell: bash run: | @@ -469,7 +469,7 @@ jobs: - name: Build Python 3.9 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.9 shell: bash run: | @@ -483,7 +483,7 @@ jobs: - name: Build Python 3.8 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.8 shell: bash run: | From 95f16ea778a3df0443151bea34b456bc51cc5a83 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 21 Jul 2025 12:18:20 +0200 Subject: [PATCH 078/291] workflow: Update to MSVC 14.3 / 2022 --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 166b627189..3a341a39a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: - name: Build Python 3.13 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.13 shell: bash run: | @@ -53,7 +53,7 @@ jobs: - name: Build Python 3.12 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.12 shell: bash run: | @@ -67,7 +67,7 @@ jobs: - name: Build Python 3.11 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.11 shell: bash run: | @@ -81,7 +81,7 @@ jobs: - name: Build Python 3.10 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.10 shell: bash run: | @@ -95,7 +95,7 @@ jobs: - name: Build Python 3.9 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.9 shell: bash run: | @@ -109,7 +109,7 @@ jobs: - name: Build Python 3.8 shell: bash run: | - python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.2 + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 - name: Test Python 3.8 shell: bash run: | From 20c9b0d90259c4cea57fc7e83716d5907963c80d Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 10:00:22 +0200 Subject: [PATCH 079/291] workflow: Hack workaround for Ubuntu CI error --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a341a39a5..d3eaf4d7ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,4 +118,5 @@ jobs: - name: Make installer run: | + rm -f built/bin/p3dcparse python makepanda/makepackage.py --verbose --lzma From de4865b3241ded61a88d0501b8dfeac2d1052b31 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 10:01:11 +0200 Subject: [PATCH 080/291] putil: grab GIL while decreffing function --- panda/src/putil/pythonCallbackObject.cxx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/panda/src/putil/pythonCallbackObject.cxx b/panda/src/putil/pythonCallbackObject.cxx index 3e185e8514..3841abcb9d 100644 --- a/panda/src/putil/pythonCallbackObject.cxx +++ b/panda/src/putil/pythonCallbackObject.cxx @@ -60,7 +60,18 @@ PythonCallbackObject(PyObject *function) { */ PythonCallbackObject:: ~PythonCallbackObject() { + // This may be called from the cull or draw thread, so we have to be sure we + // own the GIL. +#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS) + PyGILState_STATE gstate; + gstate = PyGILState_Ensure(); +#endif + Py_DECREF(_function); + +#if defined(HAVE_THREADS) && !defined(SIMPLE_THREADS) + PyGILState_Release(gstate); +#endif } /** From 39c620d1c7c55d686a6c9fa1683bb0f15988bceb Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 10:01:59 +0200 Subject: [PATCH 081/291] makepackage: don't use deprecated dpkg-shlibdeps option --- makepanda/makepackage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/makepanda/makepackage.py b/makepanda/makepackage.py index a46684d7d0..0aff2cd8d3 100755 --- a/makepanda/makepackage.py +++ b/makepanda/makepackage.py @@ -335,8 +335,8 @@ def MakeInstallerLinux(version, debversion=None, rpmversion=None, rpmrelease=1, # Library dependencies are required, binary dependencies are recommended. # We explicitly exclude libphysx-extras since we don't want to depend on PhysX. - oscmd(f"cd targetroot && LD_LIBRARY_PATH=usr/{lib_dir}/panda3d {dpkg_shlibdeps} -Tdebian/substvars_dep --ignore-missing-info -x{pkg_name} -xlibphysx-extras {lib_pattern}") - oscmd(f"cd targetroot && LD_LIBRARY_PATH=usr/{lib_dir}/panda3d {dpkg_shlibdeps} -Tdebian/substvars_rec --ignore-missing-info -x{pkg_name} {bin_pattern}") + oscmd(f"cd targetroot && {dpkg_shlibdeps} -lusr/{lib_dir}/panda3d -Tdebian/substvars_dep --ignore-missing-info -x{pkg_name} -xlibphysx-extras {lib_pattern}") + oscmd(f"cd targetroot && {dpkg_shlibdeps} -lusr/{lib_dir}/panda3d -Tdebian/substvars_rec --ignore-missing-info -x{pkg_name} {bin_pattern}") # Parse the substvars files generated by dpkg-shlibdeps. depends = ReadFile("targetroot/debian/substvars_dep").replace("shlibs:Depends=", "").strip() From 1f85588dd4065ec2a0a62d898ab5f66dd98fbdab Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 10:04:26 +0200 Subject: [PATCH 082/291] workflow: Update to Windows 2022 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f734840785..88c8ef6308 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -372,7 +372,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: - os: [ubuntu-22.04, windows-2019, macOS-13] + os: [ubuntu-22.04, windows-2022, macOS-13] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From 2767bddbe6e615b19f2ef4f51451585595828c5b Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 10:05:10 +0200 Subject: [PATCH 083/291] x11: Add config vars to disable usage of X11 extensions --- panda/src/x11display/config_x11display.cxx | 21 ++++++++++++ panda/src/x11display/config_x11display.h | 4 +++ panda/src/x11display/x11GraphicsPipe.cxx | 40 +++++++++++++++++----- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/panda/src/x11display/config_x11display.cxx b/panda/src/x11display/config_x11display.cxx index 6b8e03d27c..cfdea4e70c 100644 --- a/panda/src/x11display/config_x11display.cxx +++ b/panda/src/x11display/config_x11display.cxx @@ -45,6 +45,27 @@ ConfigVariableBool x_init_threads PRC_DESC("Set this true to ask Panda3D to call XInitThreads() upon loading " "the display module, which may help with some threading issues.")); +ConfigVariableBool x_support_xcursor +("x-support-xcursor", true, + PRC_DESC("Set this false if you wish to disable loading of the XCursor " + "library, which is used for setting custom cursor icons.")); + +ConfigVariableBool x_support_xinput2 +("x-support-xinput2", true, + PRC_DESC("Set this false if you wish to disable loading of the XInput2 " + "library, which is used for raw mouse events as well as for relative " + "mouse events if the xf86dga extension is not supported.")); + +ConfigVariableBool x_support_xf86dga +("x-support-xf86dga", true, + PRC_DESC("Set this false if you wish to disable loading of the xf86dga " + "extension, which is used for relative mouse mode.")); + +ConfigVariableBool x_support_xrandr +("x-support-xrandr", true, + PRC_DESC("Set this false if you wish to disable loading of the Xrandr " + "extension, which is used for changing screen resolutions.")); + ConfigVariableInt x_wheel_up_button ("x-wheel-up-button", 4, PRC_DESC("This is the mouse button index of the wheel_up event: which " diff --git a/panda/src/x11display/config_x11display.h b/panda/src/x11display/config_x11display.h index 351f254640..2dcc23942e 100644 --- a/panda/src/x11display/config_x11display.h +++ b/panda/src/x11display/config_x11display.h @@ -27,6 +27,10 @@ extern EXPCL_PANDAX11 void init_libx11display(); extern ConfigVariableString display_cfg; extern ConfigVariableBool x_error_abort; extern ConfigVariableBool x_init_threads; +extern ConfigVariableBool x_support_xcursor; +extern ConfigVariableBool x_support_xinput2; +extern ConfigVariableBool x_support_xf86dga; +extern ConfigVariableBool x_support_xrandr; extern ConfigVariableInt x_wheel_up_button; extern ConfigVariableInt x_wheel_down_button; diff --git a/panda/src/x11display/x11GraphicsPipe.cxx b/panda/src/x11display/x11GraphicsPipe.cxx index dda30ff5c1..136ebaee6d 100644 --- a/panda/src/x11display/x11GraphicsPipe.cxx +++ b/panda/src/x11display/x11GraphicsPipe.cxx @@ -121,15 +121,22 @@ x11GraphicsPipe(const std::string &display) : _is_valid = true; // Dynamically load the xf86dga extension. - void *xf86dga = dlopen("libXxf86dga.so.1", RTLD_NOW | RTLD_LOCAL); - if (xf86dga != nullptr) { + if (!x_support_xf86dga) { + _XF86DGADirectVideo = nullptr; + if (x11display_cat.is_debug()) { + x11display_cat.debug() + << "XFree86-DGA support is disabled" + << "; relative mouse mode may not work.\n"; + } + } else if (void *xf86dga = dlopen("libXxf86dga.so.1", RTLD_NOW | RTLD_LOCAL)) { pfn_XF86DGAQueryVersion _XF86DGAQueryVersion = (pfn_XF86DGAQueryVersion)dlsym(xf86dga, "XF86DGAQueryVersion"); _XF86DGADirectVideo = (pfn_XF86DGADirectVideo)dlsym(xf86dga, "XF86DGADirectVideo"); int major_ver, minor_ver; if (_XF86DGAQueryVersion == nullptr || _XF86DGADirectVideo == nullptr) { x11display_cat.warning() - << "libXxf86dga.so.1 does not provide required functions; relative mouse mode may not work.\n"; + << "libXxf86dga.so.1 does not provide required functions" + << "; relative mouse mode may not work.\n"; } else if (!_XF86DGAQueryVersion(_display, &major_ver, &minor_ver)) { _XF86DGADirectVideo = nullptr; @@ -138,13 +145,19 @@ x11GraphicsPipe(const std::string &display) : _XF86DGADirectVideo = nullptr; if (x11display_cat.is_debug()) { x11display_cat.debug() - << "cannot dlopen libXxf86dga.so.1; relative mouse mode may not work.\n"; + << "cannot dlopen libXxf86dga.so.1" + << "; relative mouse mode may not work.\n"; } } // Dynamically load the XCursor extension. - void *xcursor = dlopen("libXcursor.so.1", RTLD_NOW | RTLD_LOCAL); - if (xcursor != nullptr) { + if (!x_support_xcursor) { + _xcursor_size = -1; + if (x11display_cat.is_debug()) { + x11display_cat.debug() + << "XCursor support is disabled; cursor changing will not work.\n"; + } + } else if (void *xcursor = dlopen("libXcursor.so.1", RTLD_NOW | RTLD_LOCAL)) { pfn_XcursorGetDefaultSize _XcursorGetDefaultSize = (pfn_XcursorGetDefaultSize)dlsym(xcursor, "XcursorGetDefaultSize"); _XcursorXcFileLoadImages = (pfn_XcursorXcFileLoadImages)dlsym(xcursor, "XcursorXcFileLoadImages"); _XcursorImagesLoadCursor = (pfn_XcursorImagesLoadCursor)dlsym(xcursor, "XcursorImagesLoadCursor"); @@ -175,8 +188,14 @@ x11GraphicsPipe(const std::string &display) : } // Dynamically load the XRandr extension. - void *xrandr = dlopen("libXrandr.so.2", RTLD_NOW | RTLD_LOCAL); - if (xrandr != nullptr) { + if (!x_support_xrandr) { + _have_xrandr = false; + if (x11display_cat.is_debug()) { + x11display_cat.debug() + << "XRandR support is disabled; resolution setting will not work.\n"; + } + } + else if (void *xrandr = dlopen("libXrandr.so.2", RTLD_NOW | RTLD_LOCAL)) { pfn_XRRQueryExtension _XRRQueryExtension = (pfn_XRRQueryExtension)dlsym(xrandr, "XRRQueryExtension"); pfn_XRRQueryVersion _XRRQueryVersion = (pfn_XRRQueryVersion)dlsym(xrandr, "XRRQueryVersion"); @@ -238,7 +257,8 @@ x11GraphicsPipe(const std::string &display) : // Dynamically load the XInput2 extension. int ev, err; - if (XQueryExtension(_display, "XInputExtension", &_xi_opcode, &ev, &err)) { + if (x_support_xinput2 && + XQueryExtension(_display, "XInputExtension", &_xi_opcode, &ev, &err)) { void *xi = dlopen("libXi.so.6", RTLD_NOW | RTLD_LOCAL); if (xi != nullptr) { pfn_XIQueryVersion _XIQueryVersion = (pfn_XIQueryVersion)dlsym(xi, "XIQueryVersion"); @@ -272,6 +292,8 @@ x11GraphicsPipe(const std::string &display) : << "cannot dlopen libXi.so.1; relative mouse mode will not work.\n"; } } + } else { + _XISelectEvents = nullptr; } // Use Xrandr to fill in the supported resolution list. From 6a9dde43dab53c4c1a9ab291d37ecd091fc5c061 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 11:10:15 +0200 Subject: [PATCH 084/291] workflow: Fix Windows build --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3eaf4d7ef..2feb1aa06e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,7 +116,10 @@ jobs: python -m pip install pytest PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest - - name: Make installer + - name: Remove p3dcparse + if: matrix.os == 'ubuntu-22.04' run: | rm -f built/bin/p3dcparse + - name: Make installer + run: | python makepanda/makepackage.py --verbose --lzma From 7d7a08e66f56be6bbba63fe3fd364360b864acd7 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 11:09:07 +0200 Subject: [PATCH 085/291] cppparser: Consider reference types destructible Fixes #1743 --- dtool/src/cppparser/cppReferenceType.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtool/src/cppparser/cppReferenceType.cxx b/dtool/src/cppparser/cppReferenceType.cxx index 42526789a3..c2b914caf2 100644 --- a/dtool/src/cppparser/cppReferenceType.cxx +++ b/dtool/src/cppparser/cppReferenceType.cxx @@ -187,7 +187,7 @@ is_copy_constructible() const { */ bool CPPReferenceType:: is_destructible() const { - return false; + return true; } /** From ddcfee18b44fe77bf434390a957a160efabf7c9f Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Mon, 28 Jul 2025 03:15:23 -0600 Subject: [PATCH 086/291] showbase: Annotate methods of Event, Interval, and Job Managers (#1762) --- direct/src/interval/IntervalManager.py | 17 ++++++----- direct/src/showbase/EventManager.py | 41 ++++++++++++++++++-------- direct/src/showbase/JobManager.py | 22 ++++++++------ 3 files changed, 52 insertions(+), 28 deletions(-) diff --git a/direct/src/interval/IntervalManager.py b/direct/src/interval/IntervalManager.py index f3e3f7d13d..57476dc347 100644 --- a/direct/src/interval/IntervalManager.py +++ b/direct/src/interval/IntervalManager.py @@ -1,11 +1,14 @@ """Defines the IntervalManager class as well as the global instance of this class, ivalMgr.""" +from __future__ import annotations + __all__ = ['IntervalManager', 'ivalMgr'] from panda3d.core import EventQueue -from panda3d.direct import CIntervalManager, Dtool_BorrowThisReference +from panda3d.direct import CInterval, CIntervalManager, Dtool_BorrowThisReference from direct.showbase import EventManager +from . import Interval import fnmatch class IntervalManager(CIntervalManager): @@ -15,7 +18,7 @@ class IntervalManager(CIntervalManager): # the Python extensions is to add support for Python-based # intervals (like MetaIntervals). - def __init__(self, globalPtr = 0): + def __init__(self, globalPtr: bool = False) -> None: # Pass globalPtr == 1 to the constructor to trick it into # "constructing" a Python wrapper around the global # CIntervalManager object. @@ -28,8 +31,8 @@ class IntervalManager(CIntervalManager): self.eventQueue = EventQueue() self.MyEventmanager = EventManager.EventManager(self.eventQueue) self.setEventQueue(self.eventQueue) - self.ivals = [] - self.removedIvals = {} + self.ivals: list[Interval.Interval | CInterval | None] = [] + self.removedIvals: dict = {} def addInterval(self, interval): index = self.addCInterval(interval, 1) @@ -86,7 +89,7 @@ class IntervalManager(CIntervalManager): ival.pause() return len(ivals) - def step(self): + def step(self) -> None: # This method should be called once per frame to perform all # of the per-frame processing on the active intervals. # Call C++ step, then do the Python stuff. @@ -101,7 +104,7 @@ class IntervalManager(CIntervalManager): CIntervalManager.interrupt(self) self.__doPythonCallbacks() - def __doPythonCallbacks(self): + def __doPythonCallbacks(self) -> None: # This method does all of the required Python post-processing # after performing some C++-level action. # It is important to call all of the python callbacks on the @@ -137,4 +140,4 @@ class IntervalManager(CIntervalManager): self.ivals[index] = interval #: The global IntervalManager object. -ivalMgr = IntervalManager(1) +ivalMgr = IntervalManager(True) diff --git a/direct/src/showbase/EventManager.py b/direct/src/showbase/EventManager.py index e603633810..7bbb915915 100644 --- a/direct/src/showbase/EventManager.py +++ b/direct/src/showbase/EventManager.py @@ -1,21 +1,33 @@ """Contains the EventManager class. See :mod:`.EventManagerGlobal` for the global eventMgr instance.""" +from __future__ import annotations + __all__ = ['EventManager'] +from typing import Any + from direct.directnotify.DirectNotifyGlobal import directNotify +from direct.directnotify.Notifier import Notifier from direct.task.TaskManagerGlobal import taskMgr from direct.showbase.MessengerGlobal import messenger -from panda3d.core import PStatCollector, EventQueue, EventHandler -from panda3d.core import ConfigVariableBool +from panda3d.core import ( + ConfigVariableBool, + Event, + EventHandler, + EventParameter, + EventQueue, + PStatCollector, + PythonTask, +) class EventManager: - notify = None + notify: Notifier | None = None - def __init__(self, eventQueue = None): + def __init__(self, eventQueue: EventQueue | None = None) -> None: """ Create a C++ event queue and handler """ @@ -24,11 +36,11 @@ class EventManager: EventManager.notify = directNotify.newCategory("EventManager") self.eventQueue = eventQueue - self.eventHandler = None + self.eventHandler: EventHandler | None = None self._wantPstats = ConfigVariableBool('pstats-eventmanager', False) - def doEvents(self): + def doEvents(self) -> None: """ Process all the events on the C++ event queue """ @@ -38,12 +50,13 @@ class EventManager: processFunc = self.processEventPstats else: processFunc = self.processEvent + assert self.eventQueue is not None isEmptyFunc = self.eventQueue.isQueueEmpty dequeueFunc = self.eventQueue.dequeueEvent while not isEmptyFunc(): processFunc(dequeueFunc()) - def eventLoopTask(self, task): + def eventLoopTask(self, task: PythonTask) -> int: """ Process all the events on the C++ event queue """ @@ -51,7 +64,7 @@ class EventManager: messenger.send("event-loop-done") return task.cont - def parseEventParameter(self, eventParameter): + def parseEventParameter(self, eventParameter: EventParameter) -> Any: """ Extract the actual data from the eventParameter """ @@ -72,7 +85,7 @@ class EventManager: # which will be downcast to that type. return eventParameter.getPtr() - def processEvent(self, event): + def processEvent(self, event: Event) -> None: """ Process a C++ event Duplicate any changes in processEventPstats @@ -89,6 +102,7 @@ class EventManager: paramList.append(eventParameterData) # Do not print the new frame debug, it is too noisy! + assert EventManager.notify is not None if EventManager.notify.getDebug() and eventName != 'NewFrame': EventManager.notify.debug('received C++ event named: ' + eventName + ' parameters: ' + repr(paramList)) @@ -106,9 +120,10 @@ class EventManager: else: # An unnamed event from C++ is probably a bad thing + assert EventManager.notify is not None EventManager.notify.warning('unnamed event in processEvent') - def processEventPstats(self, event): + def processEventPstats(self, event: Event) -> None: """ Process a C++ event with pstats tracking Duplicate any changes in processEvent @@ -125,6 +140,7 @@ class EventManager: paramList.append(eventParameterData) # Do not print the new frame debug, it is too noisy! + assert EventManager.notify is not None if EventManager.notify.getDebug() and eventName != 'NewFrame': EventManager.notify.debug('received C++ event named: ' + eventName + ' parameters: ' + repr(paramList)) @@ -156,9 +172,10 @@ class EventManager: else: # An unnamed event from C++ is probably a bad thing + assert EventManager.notify is not None EventManager.notify.warning('unnamed event in processEvent') - def restart(self): + def restart(self) -> None: if self.eventQueue is None: self.eventQueue = EventQueue.getGlobalEventQueue() @@ -173,7 +190,7 @@ class EventManager: taskMgr.add(self.eventLoopTask, 'eventManager') - def shutdown(self): + def shutdown(self) -> None: taskMgr.remove('eventManager') # Flush the event queue. We do this after removing the task diff --git a/direct/src/showbase/JobManager.py b/direct/src/showbase/JobManager.py index e6f034f08c..c491be941d 100755 --- a/direct/src/showbase/JobManager.py +++ b/direct/src/showbase/JobManager.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from collections.abc import Iterator + from panda3d.core import ConfigVariableBool, ConfigVariableDouble, ClockObject from direct.directnotify.DirectNotifyGlobal import directNotify from direct.task.TaskManagerGlobal import taskMgr @@ -17,28 +21,28 @@ class JobManager: # there's one task for the JobManager, all jobs run in this task TaskName = 'jobManager' - def __init__(self, timeslice=None): + def __init__(self, timeslice: float | None = None) -> None: # how long do we run per frame self._timeslice = timeslice # store the jobs in these structures to allow fast lookup by various keys # priority -> jobId -> job - self._pri2jobId2job = {} + self._pri2jobId2job: dict[int, dict[int, Job]] = {} # priority -> chronological list of jobIds - self._pri2jobIds = {} + self._pri2jobIds: dict[int, list[int]] = {} # jobId -> priority - self._jobId2pri = {} + self._jobId2pri: dict[int, int] = {} # how many timeslices to give each job; this is used to efficiently implement # the relative job priorities - self._jobId2timeslices = {} + self._jobId2timeslices: dict[int, int] = {} # how much time did the job use beyond the allotted timeslice, used to balance # out CPU usage - self._jobId2overflowTime = {} - self._useOverflowTime = None + self._jobId2overflowTime: dict[int, float] = {} + self._useOverflowTime: bool | None = None # this is a generator that we use to give high-priority jobs more timeslices, # it yields jobIds in a sequence that includes high-priority jobIds more often # than low-priority - self._jobIdGenerator = None - self._highestPriority = Job.Priorities.Normal + self._jobIdGenerator: Iterator[int] | None = None + self._highestPriority: int = Job.Priorities.Normal # type: ignore[attr-defined] def destroy(self): taskMgr.remove(JobManager.TaskName) From 41e4cf5d11409c71512996ec3198652543b8a74a Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 14:54:08 +0200 Subject: [PATCH 087/291] event: Fix `await gather()` returning first item instead of tuple Fixes #1738 --- panda/src/event/asyncFuture_ext.cxx | 23 ++++++++++++++++++++-- tests/event/test_futures.py | 30 ++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/panda/src/event/asyncFuture_ext.cxx b/panda/src/event/asyncFuture_ext.cxx index 525757d618..28753dd924 100644 --- a/panda/src/event/asyncFuture_ext.cxx +++ b/panda/src/event/asyncFuture_ext.cxx @@ -167,8 +167,27 @@ static PyObject *gen_next(PyObject *self) { } else { PyObject *result = get_done_result(future); if (result != nullptr) { - Py_INCREF(PyExc_StopIteration); - PyErr_Restore(PyExc_StopIteration, result, nullptr); + // See python/cpython#101578 - PyErr_SetObject has a special case where + // it interprets a tuple specially, so we bypass that by creating the + // exception directly. +#if PY_VERSION_HEX >= 0x030C0000 // 3.12 + PyObject *exc = PyObject_CallOneArg(PyExc_StopIteration, result); + if (LIKELY(exc != nullptr)) { + // This function steals a reference to exc. + PyErr_SetRaisedException(exc); + } +#else + if (PyTuple_Check(result)) { + PyObject *exc = PyObject_CallOneArg(PyExc_StopIteration, result); + if (LIKELY(exc != nullptr)) { + PyErr_SetObject(PyExc_StopIteration, exc); + Py_DECREF(exc); + } + } else { + Py_INCREF(PyExc_StopIteration); + PyErr_Restore(PyExc_StopIteration, result, nullptr); + } +#endif } return nullptr; } diff --git a/tests/event/test_futures.py b/tests/event/test_futures.py index 2334549eef..db016ae8d3 100644 --- a/tests/event/test_futures.py +++ b/tests/event/test_futures.py @@ -10,6 +10,21 @@ else: CancelledError = Exception +def check_result(fut, expected): + """Asserts the result of the future is the expected value.""" + + if fut.result() != expected: + return False + + # Make sure that await also returns the values properly + with pytest.raises(StopIteration) as e: + next(fut.__await__()) + if e.value.value != expected: + return False + + return True + + def test_future_cancelled(): fut = core.AsyncFuture() @@ -205,15 +220,20 @@ def test_future_result(): ep = core.EventParameter(0.5) fut = core.AsyncFuture() fut.set_result(ep) - assert fut.result() == 0.5 - assert fut.result() == 0.5 + assert check_result(fut, 0.5) + assert check_result(fut, 0.5) # Store TypedObject dg = core.Datagram(b"test") fut = core.AsyncFuture() fut.set_result(dg) - assert fut.result() == dg - assert fut.result() == dg + assert check_result(fut, dg) + assert check_result(fut, dg) + + # Store tuple + fut = core.AsyncFuture() + fut.set_result((1, 2)) + assert check_result(fut, (1, 2)) # Store arbitrary Python object obj = object() @@ -250,7 +270,7 @@ def test_future_gather(): assert gather.done() assert not gather.cancelled() - assert tuple(gather.result()) == (1, 2) + assert check_result(gather, (1, 2)) def test_future_gather_cancel_inner(): From 6ef4699a486a1f9a9eecd48abf9f4eb395cc415b Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 15:10:31 +0200 Subject: [PATCH 088/291] collide: Fix potential use-after-free with transform cache disabled Fixes #1733 --- panda/src/collide/collisionBox.cxx | 15 +++++++++----- panda/src/collide/collisionCapsule.cxx | 15 +++++++++----- panda/src/collide/collisionFloorMesh.cxx | 8 ++++++-- panda/src/collide/collisionInvSphere.cxx | 18 +++++++++++------ panda/src/collide/collisionPlane.cxx | 21 +++++++++++++------- panda/src/collide/collisionPolygon.cxx | 18 +++++++++++------ panda/src/collide/collisionSphere.cxx | 25 +++++++++++++++--------- 7 files changed, 80 insertions(+), 40 deletions(-) diff --git a/panda/src/collide/collisionBox.cxx b/panda/src/collide/collisionBox.cxx index 5ec26a705e..e14030212e 100644 --- a/panda/src/collide/collisionBox.cxx +++ b/panda/src/collide/collisionBox.cxx @@ -419,7 +419,8 @@ test_intersection_from_line(const CollisionEntry &entry) const { const CollisionLine *line; DCAST_INTO_R(line, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = line->get_origin() * wrt_mat; LVector3 from_direction = line->get_direction() * wrt_mat; @@ -462,7 +463,8 @@ PT(CollisionEntry) CollisionBox:: test_intersection_from_ray(const CollisionEntry &entry) const { const CollisionRay *ray; DCAST_INTO_R(ray, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = ray->get_origin() * wrt_mat; LVector3 from_direction = ray->get_direction() * wrt_mat; @@ -511,7 +513,8 @@ PT(CollisionEntry) CollisionBox:: test_intersection_from_segment(const CollisionEntry &entry) const { const CollisionSegment *seg; DCAST_INTO_R(seg, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = seg->get_point_a() * wrt_mat; LPoint3 from_extent = seg->get_point_b() * wrt_mat; @@ -566,7 +569,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const { const CollisionCapsule *capsule; DCAST_INTO_R(capsule, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = capsule->get_point_a() * wrt_mat; LPoint3 from_b = capsule->get_point_b() * wrt_mat; @@ -711,7 +715,8 @@ test_intersection_from_box(const CollisionEntry &entry) const { const CollisionBox *box; DCAST_INTO_R(box, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 diff = wrt_mat.xform_point_general(box->get_center()) - _center; LVector3 from_extents = box->get_dimensions() * 0.5f; diff --git a/panda/src/collide/collisionCapsule.cxx b/panda/src/collide/collisionCapsule.cxx index 91ab319735..5a0a344552 100644 --- a/panda/src/collide/collisionCapsule.cxx +++ b/panda/src/collide/collisionCapsule.cxx @@ -226,7 +226,8 @@ test_intersection_from_line(const CollisionEntry &entry) const { const CollisionLine *line; DCAST_INTO_R(line, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = line->get_origin() * wrt_mat; LVector3 from_direction = line->get_direction() * wrt_mat; @@ -274,7 +275,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const { const CollisionRay *ray; DCAST_INTO_R(ray, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = ray->get_origin() * wrt_mat; LVector3 from_direction = ray->get_direction() * wrt_mat; @@ -335,7 +337,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const { const CollisionSegment *segment; DCAST_INTO_R(segment, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = segment->get_point_a() * wrt_mat; LPoint3 from_b = segment->get_point_b() * wrt_mat; @@ -402,7 +405,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const { LPoint3 into_a = _a; LVector3 into_direction = _b - into_a; - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = capsule->get_point_a() * wrt_mat; LPoint3 from_b = capsule->get_point_b() * wrt_mat; @@ -463,7 +467,8 @@ test_intersection_from_parabola(const CollisionEntry &entry) const { const CollisionParabola *parabola; DCAST_INTO_R(parabola, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); // Convert the parabola into local coordinate space. LParabola local_p(parabola->get_parabola()); diff --git a/panda/src/collide/collisionFloorMesh.cxx b/panda/src/collide/collisionFloorMesh.cxx index 5f0ab0cabc..a2261fd1cb 100644 --- a/panda/src/collide/collisionFloorMesh.cxx +++ b/panda/src/collide/collisionFloorMesh.cxx @@ -130,7 +130,9 @@ PT(CollisionEntry) CollisionFloorMesh:: test_intersection_from_ray(const CollisionEntry &entry) const { const CollisionRay *ray; DCAST_INTO_R(ray, entry.get_from(), nullptr); - LPoint3 from_origin = ray->get_origin() * entry.get_wrt_mat(); + + CPT(TransformState) wrt_space = entry.get_wrt_space(); + LPoint3 from_origin = ray->get_origin() * wrt_space->get_mat(); double fx = from_origin[0]; double fy = from_origin[1]; @@ -195,7 +197,9 @@ PT(CollisionEntry) CollisionFloorMesh:: test_intersection_from_sphere(const CollisionEntry &entry) const { const CollisionSphere *sphere; DCAST_INTO_R(sphere, entry.get_from(), nullptr); - LPoint3 from_origin = sphere->get_center() * entry.get_wrt_mat(); + + CPT(TransformState) wrt_space = entry.get_wrt_space(); + LPoint3 from_origin = sphere->get_center() * wrt_space->get_mat(); double fx = from_origin[0]; double fy = from_origin[1]; diff --git a/panda/src/collide/collisionInvSphere.cxx b/panda/src/collide/collisionInvSphere.cxx index 1428078d09..097de7245f 100644 --- a/panda/src/collide/collisionInvSphere.cxx +++ b/panda/src/collide/collisionInvSphere.cxx @@ -94,7 +94,8 @@ test_intersection_from_sphere(const CollisionEntry &entry) const { const CollisionSphere *sphere; DCAST_INTO_R(sphere, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_center = sphere->get_center() * wrt_mat; LVector3 from_radius_v = @@ -146,7 +147,8 @@ test_intersection_from_line(const CollisionEntry &entry) const { const CollisionLine *line; DCAST_INTO_R(line, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = line->get_origin() * wrt_mat; LVector3 from_direction = line->get_direction() * wrt_mat; @@ -187,7 +189,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const { const CollisionRay *ray; DCAST_INTO_R(ray, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = ray->get_origin() * wrt_mat; LVector3 from_direction = ray->get_direction() * wrt_mat; @@ -230,7 +233,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const { const CollisionSegment *segment; DCAST_INTO_R(segment, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = segment->get_point_a() * wrt_mat; LPoint3 from_b = segment->get_point_b() * wrt_mat; @@ -296,7 +300,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const { const CollisionCapsule *capsule; DCAST_INTO_R(capsule, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = capsule->get_point_a() * wrt_mat; LPoint3 from_b = capsule->get_point_b() * wrt_mat; @@ -351,7 +356,8 @@ test_intersection_from_box(const CollisionEntry &entry) const { const CollisionBox *box; DCAST_INTO_R(box, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 center = get_center(); PN_stdfloat radius_sq = get_radius(); diff --git a/panda/src/collide/collisionPlane.cxx b/panda/src/collide/collisionPlane.cxx index 2e174d4aa8..90abeb62f9 100644 --- a/panda/src/collide/collisionPlane.cxx +++ b/panda/src/collide/collisionPlane.cxx @@ -109,7 +109,8 @@ test_intersection_from_sphere(const CollisionEntry &entry) const { const CollisionSphere *sphere; DCAST_INTO_R(sphere, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_center = sphere->get_center() * wrt_mat; LVector3 from_radius_v = @@ -148,7 +149,8 @@ test_intersection_from_line(const CollisionEntry &entry) const { const CollisionLine *line; DCAST_INTO_R(line, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = line->get_origin() * wrt_mat; LVector3 from_direction = line->get_direction() * wrt_mat; @@ -193,7 +195,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const { const CollisionRay *ray; DCAST_INTO_R(ray, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = ray->get_origin() * wrt_mat; LVector3 from_direction = ray->get_direction() * wrt_mat; @@ -245,7 +248,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const { const CollisionSegment *segment; DCAST_INTO_R(segment, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = segment->get_point_a() * wrt_mat; LPoint3 from_b = segment->get_point_b() * wrt_mat; @@ -302,7 +306,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const { const CollisionCapsule *capsule; DCAST_INTO_R(capsule, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = capsule->get_point_a() * wrt_mat; LPoint3 from_b = capsule->get_point_b() * wrt_mat; @@ -373,7 +378,8 @@ test_intersection_from_parabola(const CollisionEntry &entry) const { const CollisionParabola *parabola; DCAST_INTO_R(parabola, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); // Convert the parabola into local coordinate space. LParabola local_p(parabola->get_parabola()); @@ -439,7 +445,8 @@ test_intersection_from_box(const CollisionEntry &entry) const { const CollisionBox *box; DCAST_INTO_R(box, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_center = box->get_center() * wrt_mat; LVector3 from_extents = box->get_dimensions() * 0.5f; diff --git a/panda/src/collide/collisionPolygon.cxx b/panda/src/collide/collisionPolygon.cxx index eafffdde90..33cf66bc87 100644 --- a/panda/src/collide/collisionPolygon.cxx +++ b/panda/src/collide/collisionPolygon.cxx @@ -552,7 +552,8 @@ test_intersection_from_line(const CollisionEntry &entry) const { const CollisionLine *line; DCAST_INTO_R(line, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = line->get_origin() * wrt_mat; LVector3 from_direction = line->get_direction() * wrt_mat; @@ -620,7 +621,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const { const CollisionRay *ray; DCAST_INTO_R(ray, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = ray->get_origin() * wrt_mat; LVector3 from_direction = ray->get_direction() * wrt_mat; @@ -693,7 +695,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const { const CollisionSegment *segment; DCAST_INTO_R(segment, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = segment->get_point_a() * wrt_mat; LPoint3 from_b = segment->get_point_b() * wrt_mat; @@ -763,7 +766,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const { const CollisionCapsule *capsule; DCAST_INTO_R(capsule, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = capsule->get_point_a() * wrt_mat; LPoint3 from_b = capsule->get_point_b() * wrt_mat; @@ -962,7 +966,8 @@ test_intersection_from_parabola(const CollisionEntry &entry) const { const CollisionParabola *parabola; DCAST_INTO_R(parabola, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); // Convert the parabola into local coordinate space. LParabola local_p(parabola->get_parabola()); @@ -1049,7 +1054,8 @@ test_intersection_from_box(const CollisionEntry &entry) const { // To make things easier, transform the box into the coordinate space of the // plane. - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LMatrix4 plane_mat = wrt_mat * _to_2d_mat; LPoint3 from_center = box->get_center() * plane_mat; diff --git a/panda/src/collide/collisionSphere.cxx b/panda/src/collide/collisionSphere.cxx index 0d3275ec36..2d65f938cb 100644 --- a/panda/src/collide/collisionSphere.cxx +++ b/panda/src/collide/collisionSphere.cxx @@ -236,7 +236,8 @@ test_intersection_from_line(const CollisionEntry &entry) const { const CollisionLine *line; DCAST_INTO_R(line, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = line->get_origin() * wrt_mat; LVector3 from_direction = line->get_direction() * wrt_mat; @@ -278,10 +279,11 @@ test_intersection_from_box(const CollisionEntry &entry) const { // Instead of transforming the box into the sphere's coordinate space, we do // it the other way around. It's easier that way. - const LMatrix4 &wrt_mat = entry.get_inv_wrt_mat(); + CPT(TransformState) inv_wrt_space = entry.get_inv_wrt_space(); + const LMatrix4 &inv_wrt_mat = inv_wrt_space->get_mat(); - LPoint3 center = wrt_mat.xform_point(_center); - PN_stdfloat radius_sq = wrt_mat.xform_vec(LVector3(0, 0, _radius)).length_squared(); + LPoint3 center = inv_wrt_mat.xform_point(_center); + PN_stdfloat radius_sq = inv_wrt_mat.xform_vec(LVector3(0, 0, _radius)).length_squared(); LPoint3 box_min = box->get_min(); LPoint3 box_max = box->get_max(); @@ -330,7 +332,8 @@ test_intersection_from_box(const CollisionEntry &entry) const { PT(CollisionEntry) new_entry = new CollisionEntry(entry); // To get the interior point, clamp the sphere center to the AABB. - LPoint3 interior = entry.get_wrt_mat().xform_point(center.fmax(box_min).fmin(box_max)); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + LPoint3 interior = wrt_space->get_mat().xform_point(center.fmax(box_min).fmin(box_max)); new_entry->set_interior_point(interior); // Now extrapolate the surface point and normal from that. @@ -352,7 +355,8 @@ test_intersection_from_ray(const CollisionEntry &entry) const { const CollisionRay *ray; DCAST_INTO_R(ray, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_origin = ray->get_origin() * wrt_mat; LVector3 from_direction = ray->get_direction() * wrt_mat; @@ -399,7 +403,8 @@ test_intersection_from_segment(const CollisionEntry &entry) const { const CollisionSegment *segment; DCAST_INTO_R(segment, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = segment->get_point_a() * wrt_mat; LPoint3 from_b = segment->get_point_b() * wrt_mat; @@ -448,7 +453,8 @@ test_intersection_from_capsule(const CollisionEntry &entry) const { const CollisionCapsule *capsule; DCAST_INTO_R(capsule, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); LPoint3 from_a = capsule->get_point_a() * wrt_mat; LPoint3 from_b = capsule->get_point_b() * wrt_mat; @@ -504,7 +510,8 @@ test_intersection_from_parabola(const CollisionEntry &entry) const { const CollisionParabola *parabola; DCAST_INTO_R(parabola, entry.get_from(), nullptr); - const LMatrix4 &wrt_mat = entry.get_wrt_mat(); + CPT(TransformState) wrt_space = entry.get_wrt_space(); + const LMatrix4 &wrt_mat = wrt_space->get_mat(); // Convert the parabola into local coordinate space. LParabola local_p(parabola->get_parabola()); From 5bc9b70e86468e82b68e30ca75d5dd9b9b9e0aaf Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 10:56:14 +0200 Subject: [PATCH 089/291] event: Make cancel() for regular tasks work like remove() Fixes #1741 --- panda/src/event/pythonTask.cxx | 2 +- tests/event/test_futures.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index d2515a9312..a978b9d613 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -503,7 +503,7 @@ cancel() { --_chain->_num_awaiting_tasks; return true; } - else if (must_cancel || _fut_waiter != nullptr) { + else if (_generator != nullptr && (must_cancel || _fut_waiter != nullptr)) { // We may be polling an external future, so we still need to throw a // CancelledException and allow it to be caught. if (must_cancel) { diff --git a/tests/event/test_futures.py b/tests/event/test_futures.py index c1bf870c1c..a54037bdc7 100644 --- a/tests/event/test_futures.py +++ b/tests/event/test_futures.py @@ -133,13 +133,29 @@ def test_future_wait_cancel(): fut.result() -def test_task_cancel(): +def test_task_remove(): task_mgr = core.AsyncTaskManager.get_global_ptr() task = core.PythonTask(lambda task: task.done) task_mgr.add(task) assert not task.done() task_mgr.remove(task) + assert not task.is_alive() + assert task.done() + assert task.cancelled() + + with pytest.raises(CancelledError): + task.result() + + +def test_task_cancel(): + task_mgr = core.AsyncTaskManager.get_global_ptr() + task = core.PythonTask(lambda task: task.done) + task_mgr.add(task) + + assert not task.done() + task.cancel() + assert not task.is_alive() assert task.done() assert task.cancelled() From 535105c373a7690c2fdd7cf7978f66331a8ee81d Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 13:51:10 +0200 Subject: [PATCH 090/291] build: Update to interrogate 0.6.0 Fixes #1743 --- dtool/Config.cmake | 2 +- dtool/src/interrogatedb/py_panda.h | 1 + makepanda/makepandacore.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dtool/Config.cmake b/dtool/Config.cmake index 67ea64c9ce..fbe2d24836 100644 --- a/dtool/Config.cmake +++ b/dtool/Config.cmake @@ -265,7 +265,7 @@ if(BUILD_INTERROGATE) panda3d-interrogate GIT_REPOSITORY https://github.com/panda3d/interrogate.git - GIT_TAG d2844d994fcc465a4e22b10001d3ac5c4012b814 + GIT_TAG 502b4806427f456e5fdbf4f98f271f89622a8a9c PREFIX ${_interrogate_dir} CMAKE_ARGS diff --git a/dtool/src/interrogatedb/py_panda.h b/dtool/src/interrogatedb/py_panda.h index d57af784f7..4b28908b9a 100644 --- a/dtool/src/interrogatedb/py_panda.h +++ b/dtool/src/interrogatedb/py_panda.h @@ -235,6 +235,7 @@ EXPCL_PYPANDA PyObject *Dtool_Raise_AssertionError(); EXPCL_PYPANDA PyObject *Dtool_Raise_TypeError(const char *message); EXPCL_PYPANDA PyObject *Dtool_Raise_ArgTypeError(PyObject *obj, int param, const char *function_name, const char *type_name); EXPCL_PYPANDA PyObject *Dtool_Raise_AttributeError(PyObject *obj, const char *attribute); +EXPCL_PYPANDA int Dtool_Raise_CantDeleteAttributeError(const char *attribute); EXPCL_PYPANDA PyObject *_Dtool_Raise_BadArgumentsError(); EXPCL_PYPANDA PyObject *_Dtool_Raise_BadArgumentsError(const char *message); diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index c3c101486d..cddf8f5e4a 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -589,8 +589,8 @@ def GetInterrogateDir(): return INTERROGATE_DIR dir = os.path.join(GetOutputDir(), "tmp", "interrogate") - if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.5.0.dist-info")): - oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.5.0" % (sys.executable, dir)) + if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.6.0.dist-info")): + oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.6.0" % (sys.executable, dir)) INTERROGATE_DIR = dir From a70d7c8012454ab201100db7143a1ec22e559ffe Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 15:19:14 +0200 Subject: [PATCH 091/291] collide: get_wrt_mat() et al should return a copy Fixes #1725 --- panda/src/collide/collisionEntry.I | 6 +++--- panda/src/collide/collisionEntry.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/panda/src/collide/collisionEntry.I b/panda/src/collide/collisionEntry.I index 41a591c90e..8525182028 100644 --- a/panda/src/collide/collisionEntry.I +++ b/panda/src/collide/collisionEntry.I @@ -291,7 +291,7 @@ get_wrt_prev_space() const { /** * Returns the relative transform of the from node as seen from the into node. */ -INLINE const LMatrix4 &CollisionEntry:: +INLINE LMatrix4 CollisionEntry:: get_wrt_mat() const { return get_wrt_space()->get_mat(); } @@ -299,7 +299,7 @@ get_wrt_mat() const { /** * Returns the relative transform of the into node as seen from the from node. */ -INLINE const LMatrix4 &CollisionEntry:: +INLINE LMatrix4 CollisionEntry:: get_inv_wrt_mat() const { return get_inv_wrt_space()->get_mat(); } @@ -309,7 +309,7 @@ get_inv_wrt_mat() const { * as of the previous frame (according to set_prev_transform(), * set_fluid_pos(), etc.) */ -INLINE const LMatrix4 &CollisionEntry:: +INLINE LMatrix4 CollisionEntry:: get_wrt_prev_mat() const { return get_wrt_prev_space()->get_mat(); } diff --git a/panda/src/collide/collisionEntry.h b/panda/src/collide/collisionEntry.h index 03d39c03d5..a66e037e05 100644 --- a/panda/src/collide/collisionEntry.h +++ b/panda/src/collide/collisionEntry.h @@ -109,9 +109,9 @@ public: INLINE CPT(TransformState) get_inv_wrt_space() const; INLINE CPT(TransformState) get_wrt_prev_space() const; - INLINE const LMatrix4 &get_wrt_mat() const; - INLINE const LMatrix4 &get_inv_wrt_mat() const; - INLINE const LMatrix4 &get_wrt_prev_mat() const; + INLINE LMatrix4 get_wrt_mat() const; + INLINE LMatrix4 get_inv_wrt_mat() const; + INLINE LMatrix4 get_wrt_prev_mat() const; INLINE const ClipPlaneAttrib *get_into_clip_planes() const; From 85e4d0734d088a941895074b08967761374d1ef1 Mon Sep 17 00:00:00 2001 From: omg-really Date: Mon, 28 Jul 2025 15:14:47 +0200 Subject: [PATCH 092/291] bam2egg: Fix support for more than two tags Fixes #1725 --- panda/src/egg2pg/eggSaver.cxx | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/panda/src/egg2pg/eggSaver.cxx b/panda/src/egg2pg/eggSaver.cxx index 6dbcba16d2..23e932782b 100644 --- a/panda/src/egg2pg/eggSaver.cxx +++ b/panda/src/egg2pg/eggSaver.cxx @@ -1196,34 +1196,16 @@ apply_state_properties(EggRenderMode *egg_render_mode, const RenderState *state) */ bool EggSaver:: apply_tags(EggGroup *egg_group, PandaNode *node) { - std::ostringstream strm; - char delimiter = '\n'; - string delimiter_str(1, delimiter); - node->list_tags(strm, delimiter_str); - - string data = strm.str(); - if (data.empty()) { - return false; - } - bool any_applied = false; - size_t p = 0; - size_t q = data.find(delimiter); - while (q != string::npos) { - string tag = data.substr(p, q); - if (apply_tag(egg_group, node, tag)) { + vector_string keys; + node->get_tag_keys(keys); + + for (size_t i = 0; i < keys.size(); ++i) { + if (apply_tag(egg_group, node, keys[i])) { any_applied = true; } - p = q + 1; - q = data.find(delimiter, p); } - - string tag = data.substr(p); - if (apply_tag(egg_group, node, tag)) { - any_applied = true; - } - return any_applied; } From 0a94971f1ae31d59498a3ea0288c7f2b74075531 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 15:25:32 +0200 Subject: [PATCH 093/291] event: Fix memory leak I just introduced in 41e4cf5 --- panda/src/event/asyncFuture_ext.cxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/panda/src/event/asyncFuture_ext.cxx b/panda/src/event/asyncFuture_ext.cxx index 28753dd924..4bc9f07378 100644 --- a/panda/src/event/asyncFuture_ext.cxx +++ b/panda/src/event/asyncFuture_ext.cxx @@ -172,6 +172,7 @@ static PyObject *gen_next(PyObject *self) { // exception directly. #if PY_VERSION_HEX >= 0x030C0000 // 3.12 PyObject *exc = PyObject_CallOneArg(PyExc_StopIteration, result); + Py_DECREF(result); if (LIKELY(exc != nullptr)) { // This function steals a reference to exc. PyErr_SetRaisedException(exc); @@ -179,6 +180,7 @@ static PyObject *gen_next(PyObject *self) { #else if (PyTuple_Check(result)) { PyObject *exc = PyObject_CallOneArg(PyExc_StopIteration, result); + Py_DECREF(result); if (LIKELY(exc != nullptr)) { PyErr_SetObject(PyExc_StopIteration, exc); Py_DECREF(exc); From ab357ea1209fd630c2a214c068d2cd8fe4d7d97c Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 15:29:56 +0200 Subject: [PATCH 094/291] tinydisplay: Try to fix SDL compile errors Fixes #1708 --- .../src/tinydisplay/tinySDLGraphicsWindow.cxx | 21 +++++++++++-------- panda/src/tinydisplay/tinySDLGraphicsWindow.h | 2 ++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/panda/src/tinydisplay/tinySDLGraphicsWindow.cxx b/panda/src/tinydisplay/tinySDLGraphicsWindow.cxx index 9c221846eb..54dd73fb15 100644 --- a/panda/src/tinydisplay/tinySDLGraphicsWindow.cxx +++ b/panda/src/tinydisplay/tinySDLGraphicsWindow.cxx @@ -43,7 +43,10 @@ TinySDLGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe, _pitch = 0; update_pixel_factor(); - add_input_device(GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard_mouse")); + PT(GraphicsWindowInputDevice) device = + GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard_mouse"); + add_input_device(device); + _input = device.p(); } /** @@ -166,35 +169,35 @@ process_events() { switch(evt.type) { case SDL_KEYDOWN: if (evt.key.keysym.unicode) { - _input_devices[0]->keystroke(evt.key.keysym.unicode); + _input->keystroke(evt.key.keysym.unicode); } button = get_keyboard_button(evt.key.keysym.sym); if (button != ButtonHandle::none()) { - _input_devices[0]->button_down(button); + _input->button_down(button); } break; case SDL_KEYUP: button = get_keyboard_button(evt.key.keysym.sym); if (button != ButtonHandle::none()) { - _input_devices[0]->button_up(button); + _input->button_up(button); } break; case SDL_MOUSEBUTTONDOWN: button = get_mouse_button(evt.button.button); - _input_devices[0]->set_pointer_in_window(evt.button.x, evt.button.y); - _input_devices[0]->button_down(button); + _input->set_pointer_in_window(evt.button.x, evt.button.y); + _input->button_down(button); break; case SDL_MOUSEBUTTONUP: button = get_mouse_button(evt.button.button); - _input_devices[0]->set_pointer_in_window(evt.button.x, evt.button.y); - _input_devices[0]->button_up(button); + _input->set_pointer_in_window(evt.button.x, evt.button.y); + _input->button_up(button); break; case SDL_MOUSEMOTION: - _input_devices[0]->set_pointer_in_window(evt.motion.x, evt.motion.y); + _input->set_pointer_in_window(evt.motion.x, evt.motion.y); break; case SDL_VIDEORESIZE: diff --git a/panda/src/tinydisplay/tinySDLGraphicsWindow.h b/panda/src/tinydisplay/tinySDLGraphicsWindow.h index 8073663ce5..dc3695a6c0 100644 --- a/panda/src/tinydisplay/tinySDLGraphicsWindow.h +++ b/panda/src/tinydisplay/tinySDLGraphicsWindow.h @@ -63,6 +63,8 @@ private: unsigned int _flags; unsigned int _pitch; + GraphicsWindowInputDevice *_input; + public: static TypeHandle get_class_type() { return _type_handle; From e57c71d8e48381fe7ba21b96d2a52c74c11bc7a1 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 15:42:11 +0200 Subject: [PATCH 095/291] event: Change result of AsyncFuture gather() to return list Better matches asyncio gather See #1738 --- panda/src/event/asyncFuture_ext.cxx | 4 ++-- tests/event/test_futures.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/panda/src/event/asyncFuture_ext.cxx b/panda/src/event/asyncFuture_ext.cxx index 470bb6f128..92b3d837d1 100644 --- a/panda/src/event/asyncFuture_ext.cxx +++ b/panda/src/event/asyncFuture_ext.cxx @@ -63,13 +63,13 @@ static PyObject *get_done_result(const AsyncFuture *future) { // If it's an AsyncGatheringFuture, get the result for each future. const AsyncGatheringFuture *gather = (const AsyncGatheringFuture *)future; Py_ssize_t num_futures = (Py_ssize_t)gather->get_num_futures(); - PyObject *results = PyTuple_New(num_futures); + PyObject *results = PyList_New(num_futures); for (Py_ssize_t i = 0; i < num_futures; ++i) { PyObject *result = get_done_result(gather->get_future((size_t)i)); if (result != nullptr) { // This steals a reference. - PyTuple_SET_ITEM(results, i, result); + PyList_SET_ITEM(results, i, result); } else { Py_DECREF(results); return nullptr; diff --git a/tests/event/test_futures.py b/tests/event/test_futures.py index 73e0627295..ee6b0f2894 100644 --- a/tests/event/test_futures.py +++ b/tests/event/test_futures.py @@ -527,7 +527,7 @@ def test_future_gather(): assert gather.done() assert not gather.cancelled() - assert check_result(gather, (1, 2)) + assert check_result(gather, [1, 2]) def test_future_gather_cancel_inner(): From bd4b3dd1a75b3465bb6bbc8ad6907753b7a46ac1 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 20:35:00 +0200 Subject: [PATCH 096/291] showbase: Rewrite VFSImporter for newer Python import system Fixes #1718 --- direct/src/showbase/VFSImporter.py | 625 +++++++++-------------------- tests/showbase/test_VFSImporter.py | 56 +++ 2 files changed, 238 insertions(+), 443 deletions(-) create mode 100644 tests/showbase/test_VFSImporter.py diff --git a/direct/src/showbase/VFSImporter.py b/direct/src/showbase/VFSImporter.py index c6e69c5ca7..3845a0444c 100644 --- a/direct/src/showbase/VFSImporter.py +++ b/direct/src/showbase/VFSImporter.py @@ -5,477 +5,262 @@ Calling the :func:`register()` function to register the import hooks should be sufficient to enable this functionality. """ -__all__ = ['register', 'sharedPackages', - 'reloadSharedPackage', 'reloadSharedPackages'] +__all__ = ['register'] -from panda3d.core import Filename, VirtualFileSystem, VirtualFileMountSystem, OFileStream, copyStream -from direct.stdpy.file import open +from panda3d.core import Filename, VirtualFileSystem, VirtualFileMountSystem +from panda3d.core import OFileStream, copy_stream import sys import marshal -import imp -import types +import _imp +import atexit +from importlib.abc import Loader, SourceLoader +from importlib.util import MAGIC_NUMBER, decode_source +from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, BYTECODE_SUFFIXES -#: The sharedPackages dictionary lists all of the "shared packages", -#: special Python packages that automatically span multiple directories -#: via magic in the VFSImporter. You can make a package "shared" -#: simply by adding its name into this dictionary (and then calling -#: reloadSharedPackages() if it's already been imported). -#: -#: When a package name is in this dictionary at import time, *all* -#: instances of the package are located along sys.path, and merged into -#: a single Python module with a __path__ setting that represents the -#: union. Thus, you can have a direct.showbase.foo in your own -#: application, and loading it won't shadow the system -#: direct.showbase.ShowBase which is in a different directory on disk. -sharedPackages = {} - -vfs = VirtualFileSystem.getGlobalPtr() - -compiledExtensions = ['pyc', 'pyo'] -if not __debug__: - # In optimized mode, we prefer loading .pyo files over .pyc files. - # We implement that by reversing the extension names. - compiledExtensions = ['pyo', 'pyc'] +vfs = VirtualFileSystem.get_global_ptr() -class VFSImporter: +def _make_spec(fullname, loader, *, is_package): + filename = loader._vfile.get_filename() + spec = ModuleSpec(fullname, loader, origin=filename.to_os_specific(), is_package=is_package) + if is_package: + spec.submodule_search_locations.append(Filename(filename.get_dirname()).to_os_specific()) + spec.has_location = True + return spec + + +class VFSFinder: """ This class serves as a Python importer to support loading Python .py and .pyc/.pyo files from Panda's Virtual File System, which allows loading Python source files from mounted .mf files (among other places). """ def __init__(self, path): - if isinstance(path, Filename): - self.dir_path = Filename(path) - else: - self.dir_path = Filename.fromOsSpecific(path) + self.path = path - def find_module(self, fullname, path = None): + def find_spec(self, fullname, path, target=None): if path is None: - dir_path = self.dir_path - else: - dir_path = path - #print >>sys.stderr, "find_module(%s), dir_path = %s" % (fullname, dir_path) + path = self.path + + #print(f"find_spec({fullname}), dir_path = {dir_path}", file=sys.stderr) basename = fullname.split('.')[-1] - path = Filename(dir_path, basename) + filename = Filename(Filename.from_os_specific(path), basename) # First, look for Python files. - filename = Filename(path) - filename.setExtension('py') - vfile = vfs.getFile(filename, True) + vfile = vfs.get_file(filename + '.py', True) if vfile: - return VFSLoader(dir_path, vfile, filename, - desc=('.py', 'r', imp.PY_SOURCE)) + loader = VFSSourceLoader(fullname, vfile) + return _make_spec(fullname, loader, is_package=False) # If there's no .py file, but there's a .pyc file, load that # anyway. - for ext in compiledExtensions: - filename = Filename(path) - filename.setExtension(ext) - vfile = vfs.getFile(filename, True) + for suffix in BYTECODE_SUFFIXES: + vfile = vfs.get_file(filename + suffix, True) if vfile: - return VFSLoader(dir_path, vfile, filename, - desc=('.'+ext, 'rb', imp.PY_COMPILED)) + loader = VFSCompiledLoader(fullname, vfile) + return _make_spec(fullname, loader, is_package=False) # Look for a C/C++ extension module. - for desc in imp.get_suffixes(): - if desc[2] != imp.C_EXTENSION: - continue - - filename = Filename(path + desc[0]) - vfile = vfs.getFile(filename, True) + for suffix in EXTENSION_SUFFIXES: + vfile = vfs.get_file(filename + suffix, True) if vfile: - return VFSLoader(dir_path, vfile, filename, desc=desc) + loader = VFSExtensionLoader(fullname, vfile) + return _make_spec(fullname, loader, is_package=False) - # Finally, consider a package, i.e. a directory containing - # __init__.py. - filename = Filename(path, '__init__.py') - vfile = vfs.getFile(filename, True) + # Consider a package, i.e. a directory containing __init__.py. + init_filename = Filename(filename, '__init__.py') + vfile = vfs.get_file(init_filename, True) if vfile: - return VFSLoader(dir_path, vfile, filename, packagePath=path, - desc=('.py', 'r', imp.PY_SOURCE)) - for ext in compiledExtensions: - filename = Filename(path, '__init__.' + ext) - vfile = vfs.getFile(filename, True) - if vfile: - return VFSLoader(dir_path, vfile, filename, packagePath=path, - desc=('.'+ext, 'rb', imp.PY_COMPILED)) + loader = VFSSourceLoader(fullname, vfile) + return _make_spec(fullname, loader, is_package=True) - #print >>sys.stderr, "not found." + for suffix in BYTECODE_SUFFIXES: + init_filename = Filename(filename, '__init__' + suffix) + vfile = vfs.get_file(init_filename, True) + if vfile: + loader = VFSCompiledLoader(fullname, vfile) + return _make_spec(fullname, loader, is_package=True) + + # Consider a namespace package. + if vfs.is_directory(filename): + spec = ModuleSpec(fullname, VFSNamespaceLoader(), is_package=True) + spec.submodule_search_locations.append(filename.to_os_specific()) + return spec + + #print("not found.", file=sys.stderr) return None -class VFSLoader: - """ The second part of VFSImporter, this is created for a - particular .py file or directory. """ - - def __init__(self, dir_path, vfile, filename, desc, packagePath=None): - self.dir_path = dir_path - self.timestamp = None - if vfile: - self.timestamp = vfile.getTimestamp() - self.filename = filename - self.desc = desc - self.packagePath = packagePath - - def load_module(self, fullname, loadingShared = False): - #print >>sys.stderr, "load_module(%s), dir_path = %s, filename = %s" % (fullname, self.dir_path, self.filename) - if self.desc[2] == imp.PY_FROZEN: - return self._import_frozen_module(fullname) - if self.desc[2] == imp.C_EXTENSION: - return self._import_extension_module(fullname) - - # Check if this is a child of a shared package. - if not loadingShared and self.packagePath and '.' in fullname: - parentname = fullname.rsplit('.', 1)[0] - if parentname in sharedPackages: - # It is. That means it's a shared package too. - parent = sys.modules[parentname] - path = getattr(parent, '__path__', None) - importer = VFSSharedImporter() - sharedPackages[fullname] = True - loader = importer.find_module(fullname, path = path) - assert loader - return loader.load_module(fullname) - - code = self._read_code() - if not code: - raise ImportError('No Python code in %s' % (fullname)) - - mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) - mod.__file__ = self.filename.toOsSpecific() - mod.__loader__ = self - if self.packagePath: - mod.__path__ = [self.packagePath.toOsSpecific()] - #print >> sys.stderr, "loaded %s, path = %s" % (fullname, mod.__path__) - - exec(code, mod.__dict__) - return sys.modules[fullname] - - def getdata(self, path): - path = Filename(self.dir_path, Filename.fromOsSpecific(path)) - vfile = vfs.getFile(path) - if not vfile: - raise IOError("Could not find '%s'" % (path)) - return vfile.readFile(True) +class VFSLoader(Loader): + def __init__(self, fullname, vfile): + self.name = fullname + self._vfile = vfile def is_package(self, fullname): - return bool(self.packagePath) + if fullname is not None and self.name != fullname: + raise ImportError - def get_code(self, fullname): - return self._read_code() + filename = self._vfile.get_filename().get_basename() + filename_base = filename.rsplit('.', 1)[0] + tail_name = fullname.rpartition('.')[2] + return filename_base == '__init__' and tail_name != '__init__' - def get_source(self, fullname): - return self._read_source() + def create_module(self, spec): + """Use default semantics for module creation.""" + + def exec_module(self, module): + """Execute the module.""" + code = self.get_code(module.__name__) + exec(code, module.__dict__) def get_filename(self, fullname): - return self.filename.toOsSpecific() + if fullname is not None and self.name != fullname: + raise ImportError - def _read_source(self): - """ Returns the Python source for this file, if it is - available, or None if it is not. May raise IOError. """ + return self._vfile.get_filename().to_os_specific() - if self.desc[2] == imp.PY_COMPILED or \ - self.desc[2] == imp.C_EXTENSION: - return None + @staticmethod + def get_data(path): + vfile = vfs.get_file(Filename.from_os_specific(path)) + if vfile: + return vfile.read_file(True) + else: + raise OSError - filename = Filename(self.filename) - filename.setExtension('py') - filename.setText() + @staticmethod + def path_stats(path): + vfile = vfs.get_file(Filename.from_os_specific(path)) + if vfile: + return {'mtime': vfile.get_timestamp(), 'size': vfile.get_file_size()} + else: + raise OSError - # Use the tokenize module to detect the encoding. - import tokenize - fh = open(self.filename, 'rb') - encoding, lines = tokenize.detect_encoding(fh.readline) - return (b''.join(lines) + fh.read()).decode(encoding) + @staticmethod + def path_mtime(path): + vfile = vfs.get_file(Filename.from_os_specific(path)) + if vfile: + return vfile.get_timestamp() + else: + raise OSError - def _import_extension_module(self, fullname): - """ Loads the binary shared object as a Python module, and - returns it. """ - vfile = vfs.getFile(self.filename, False) +class VFSSourceLoader(VFSLoader, SourceLoader): + def get_source(self, fullname): + if fullname is not None and self.name != fullname: + raise ImportError + + return decode_source(self._vfile.read_file(True)) + + +class VFSCompiledLoader(VFSLoader): + def get_code(self, fullname): + if fullname is not None and self.name != fullname: + raise ImportError + + vfile = self._vfile + data = vfile.read_file(True) + if data[:4] != MAGIC_NUMBER: + raise ImportError("Bad magic number in %s" % (vfile)) + + return marshal.loads(data[16:]) + + def get_source(self, fullname): + return None + + +class VFSExtensionLoader(VFSLoader): + def create_module(self, spec): + vfile = self._vfile + filename = vfile.get_filename() # We can only import an extension module if it already exists on # disk. This means if it's a truly virtual file that has no # on-disk equivalent, we have to write it to a temporary file # first. - if hasattr(vfile, 'getMount') and \ - isinstance(vfile.getMount(), VirtualFileMountSystem): + if isinstance(vfile.get_mount(), VirtualFileMountSystem): # It's a real file. - filename = self.filename - elif self.filename.exists(): + pass + elif filename.exists(): # It's a virtual file, but it's shadowing a real file in # the same directory. Assume they're the same, and load # the real one. - filename = self.filename - else: - # It's a virtual file with no real-world existence. Dump - # it to disk. TODO: clean up this filename. - filename = Filename.temporary('', self.filename.getBasenameWoExtension(), - '.' + self.filename.getExtension(), - type = Filename.TDso) - filename.setExtension(self.filename.getExtension()) - filename.setBinary() - sin = vfile.openReadFile(True) - sout = OFileStream() - if not filename.openWrite(sout): - raise IOError - if not copyStream(sin, sout): - raise IOError - vfile.closeReadFile(sin) - del sout - - module = imp.load_module(fullname, None, filename.toOsSpecific(), - self.desc) - module.__file__ = self.filename.toOsSpecific() - return module - - def _import_frozen_module(self, fullname): - """ Imports the frozen module without messing around with - searching any more. """ - #print >>sys.stderr, "importing frozen %s" % (fullname) - module = imp.load_module(fullname, None, fullname, - ('', '', imp.PY_FROZEN)) - module.__path__ = [] - return module - - def _read_code(self): - """ Returns the Python compiled code object for this file, if - it is available, or None if it is not. May raise IOError, - ValueError, SyntaxError, or a number of other errors generated - by the low-level system. """ - - if self.desc[2] == imp.PY_COMPILED: - # It's a pyc file; just read it directly. - pycVfile = vfs.getFile(self.filename, False) - if pycVfile: - return self._loadPyc(pycVfile, None) - raise IOError('Could not read %s' % (self.filename)) - - elif self.desc[2] == imp.C_EXTENSION: - return None - - # It's a .py file (or an __init__.py file; same thing). Read - # the .pyc file if it is available and current; otherwise read - # the .py file and compile it. - t_pyc = None - for ext in compiledExtensions: - pycFilename = Filename(self.filename) - pycFilename.setExtension(ext) - pycVfile = vfs.getFile(pycFilename, False) - if pycVfile: - t_pyc = pycVfile.getTimestamp() - break - - code = None - if t_pyc and t_pyc >= self.timestamp: - try: - code = self._loadPyc(pycVfile, self.timestamp) - except ValueError: - code = None - - if not code: - source = self._read_source() - filename = Filename(self.filename) - filename.setExtension('py') - code = self._compile(filename, source) - - return code - - def _loadPyc(self, vfile, timestamp): - """ Reads and returns the marshal data from a .pyc file. - Raises ValueError if there is a problem. """ - - code = None - data = vfile.readFile(True) - if data[:4] != imp.get_magic(): - raise ValueError("Bad magic number in %s" % (vfile)) - - t = int.from_bytes(data[4:8], 'little') - data = data[12:] - - if not timestamp or t == timestamp: - return marshal.loads(data) - else: - raise ValueError("Timestamp wrong on %s" % (vfile)) - - def _compile(self, filename, source): - """ Compiles the Python source code to a code object and - attempts to write it to an appropriate .pyc file. May raise - SyntaxError or other errors generated by the compiler. """ - - if source and source[-1] != '\n': - source = source + '\n' - code = compile(source, filename.toOsSpecific(), 'exec') - - # try to cache the compiled code - pycFilename = Filename(filename) - pycFilename.setExtension(compiledExtensions[0]) - try: - f = open(pycFilename.toOsSpecific(), 'wb') - except IOError: pass else: - f.write(imp.get_magic()) - f.write((self.timestamp & 0xffffffff).to_bytes(4, 'little')) - f.write(b'\0\0\0\0') - f.write(marshal.dumps(code)) - f.close() - - return code - - -class VFSSharedImporter: - """ This is a special importer that is added onto the meta_path - list, so that it is called before sys.path is traversed. It uses - special logic to load one of the "shared" packages, by searching - the entire sys.path for all instances of this shared package, and - merging them. """ - - def __init__(self): - pass - - def find_module(self, fullname, path = None, reload = False): - #print >>sys.stderr, "shared find_module(%s), path = %s" % (fullname, path) - - if fullname not in sharedPackages: - # Not a shared package; fall back to normal import. - return None - - if path is None: - path = sys.path - - excludePaths = [] - if reload: - # If reload is true, we are simply reloading the module, - # looking for new paths to add. - mod = sys.modules[fullname] - excludePaths = getattr(mod, '_vfs_shared_path', None) - if excludePaths is None: - # If there isn't a _vfs_shared_path symbol already, - # the module must have been loaded through - # conventional means. Try to guess which path it was - # found on. - d = self.getLoadedDirname(mod) - excludePaths = [d] - - loaders = [] - for dir in path: - if dir in excludePaths: - continue - - importer = sys.path_importer_cache.get(dir, None) - if importer is None: - try: - importer = VFSImporter(dir) - except ImportError: - continue - - sys.path_importer_cache[dir] = importer - + # It's a virtual file with no real-world existence. Dump + # it to disk. + ext = filename.get_extension() + tmp_filename = Filename.temporary('', filename.get_basename_wo_extension(), + '.' + ext, + type = Filename.T_dso) + tmp_filename.set_extension(ext) + tmp_filename.set_binary() + sin = vfile.open_read_file(True) try: - loader = importer.find_module(fullname) - if not loader: - continue - except ImportError: - continue + sout = OFileStream() + if not tmp_filename.open_write(sout): + raise IOError + if not copy_stream(sin, sout): + raise IOError + finally: + vfile.close_read_file(sin) + del sout - loaders.append(loader) + # Delete when the process ends. + atexit.register(tmp_filename.unlink) - if not loaders: - return None - return VFSSharedLoader(loaders, reload = reload) + # Make a dummy spec to pass to create_dynamic with the path to + # our temporary file. + spec = ModuleSpec(spec.name, spec.loader, + origin=tmp_filename.to_os_specific(), + is_package=False) - def getLoadedDirname(self, mod): - """ Returns the directory name that the indicated - conventionally-loaded module must have been loaded from. """ + module = _imp.create_dynamic(spec) + module.__file__ = filename.to_os_specific() + return module - if not getattr(mod, '__file__', None): - return None + def exec_module(self, module): + _imp.exec_dynamic(module) - fullname = mod.__name__ - dirname = Filename.fromOsSpecific(mod.__file__).getDirname() + def is_package(self, fullname): + return False - parentname = None - basename = fullname - if '.' in fullname: - parentname, basename = fullname.rsplit('.', 1) + def get_code(self, fullname): + return None - path = None - if parentname: - parent = sys.modules[parentname] - path = parent.__path__ - if path is None: - path = sys.path - - for dir in path: - pdir = str(Filename.fromOsSpecific(dir)) - if pdir + '/' + basename == dirname: - # We found it! - return dir - - # Couldn't figure it out. + def get_source(self, fullname): return None -class VFSSharedLoader: - """ The second part of VFSSharedImporter, this imports a list of - packages and combines them. """ +class VFSNamespaceLoader: + def create_module(self, spec): + """Use default semantics for module creation.""" - def __init__(self, loaders, reload): - self.loaders = loaders - self.reload = reload + def exec_module(self, module): + pass - def load_module(self, fullname): - #print >>sys.stderr, "shared load_module(%s), loaders = %s" % (fullname, map(lambda l: l.dir_path, self.loaders)) + def is_package(self, fullname): + return True - mod = None - message = None - path = [] - vfs_shared_path = [] - if self.reload: - mod = sys.modules[fullname] - path = mod.__path__ or [] - if path == fullname: - # Work around Python bug setting __path__ of frozen modules. - path = [] - vfs_shared_path = getattr(mod, '_vfs_shared_path', []) + def get_source(self, fullname): + return '' - for loader in self.loaders: - try: - mod = loader.load_module(fullname, loadingShared = True) - except ImportError: - etype, evalue, etraceback = sys.exc_info() - print("%s on %s: %s" % (etype.__name__, fullname, evalue)) - if not message: - message = '%s: %s' % (fullname, evalue) - continue - for dir in getattr(mod, '__path__', []): - if dir not in path: - path.append(dir) + def get_code(self, fullname): + return compile('', '', 'exec', dont_inherit=True) - if mod is None: - # If all of them failed to load, raise ImportError. - raise ImportError(message) - # If at least one of them loaded successfully, return the - # union of loaded modules. - mod.__path__ = path - mod.__package__ = fullname - - # Also set this special symbol, which records that this is a - # shared package, and also lists the paths we have already - # loaded. - mod._vfs_shared_path = vfs_shared_path + [l.dir_path for l in self.loaders] - - return mod +def _path_hook(entry): + # If this is a directory in the VFS, create a VFSFinder for this entry. + vfile = vfs.get_file(Filename.from_os_specific(entry), False) + if vfile and vfile.is_directory() and not isinstance(vfile.get_mount(), VirtualFileMountSystem): + return VFSFinder(entry) + else: + raise ImportError _registered = False - def register(): - """ Register the VFSImporter on the path_hooks, if it has not + """ Register the VFSFinder on the path_hooks, if it has not already been registered, so that future Python import statements will vector through here (and therefore will take advantage of Panda's virtual file system). """ @@ -483,55 +268,9 @@ def register(): global _registered if not _registered: _registered = True - sys.path_hooks.insert(0, VFSImporter) - sys.meta_path.insert(0, VFSSharedImporter()) + sys.path_hooks.insert(0, _path_hook) # Blow away the importer cache, so we'll come back through the - # VFSImporter for every folder in the future, even those + # VFSFinder for every folder in the future, even those # folders that previously were loaded directly. sys.path_importer_cache = {} - - -def reloadSharedPackage(mod): - """ Reloads the specific module as a shared package, adding any - new directories that might have appeared on the search path. """ - - fullname = mod.__name__ - path = None - if '.' in fullname: - parentname = fullname.rsplit('.', 1)[0] - parent = sys.modules[parentname] - path = parent.__path__ - - importer = VFSSharedImporter() - loader = importer.find_module(fullname, path = path, reload = True) - if loader: - loader.load_module(fullname) - - # Also force any child packages to become shared packages, if - # they aren't already. - for basename, child in list(mod.__dict__.items()): - if isinstance(child, types.ModuleType): - childname = child.__name__ - if childname == fullname + '.' + basename and \ - hasattr(child, '__path__') and \ - childname not in sharedPackages: - sharedPackages[childname] = True - reloadSharedPackage(child) - - -def reloadSharedPackages(): - """ Walks through the sharedPackages list, and forces a reload of - any modules on that list that have already been loaded. This - allows new directories to be added to the search path. """ - - #print >> sys.stderr, "reloadSharedPackages, path = %s, sharedPackages = %s" % (sys.path, sharedPackages.keys()) - - # Sort the list, just to make sure parent packages are reloaded - # before child packages are. - for fullname in sorted(sharedPackages.keys()): - mod = sys.modules.get(fullname, None) - if not mod: - continue - - reloadSharedPackage(mod) diff --git a/tests/showbase/test_VFSImporter.py b/tests/showbase/test_VFSImporter.py new file mode 100644 index 0000000000..33c1c6a186 --- /dev/null +++ b/tests/showbase/test_VFSImporter.py @@ -0,0 +1,56 @@ +from panda3d.core import VirtualFileSystem, VirtualFileMountRamdisk +import sys + + +def test_VFSImporter(): + from direct.showbase import VFSImporter + + VFSImporter.register() + + vfs = VirtualFileSystem.get_global_ptr() + mount = VirtualFileMountRamdisk() + success = vfs.mount(mount, "/ram", 0) + assert success + try: + sys.path.insert(0, "/ram") + vfs.write_file("/ram/testmod.py", b"var = 1\n", False) + + vfs.make_directory("/ram/testpkg") + vfs.write_file("/ram/testpkg/__init__.py", b"var = 2\n", False) + vfs.write_file("/ram/testpkg/test.py", b"var = 3\n", False) + + vfs.make_directory("/ram/testnspkg") + vfs.write_file("/ram/testnspkg/test.py", b"var = 4\n", False) + + import testmod + assert testmod.var == 1 + assert testmod.__spec__.name == 'testmod' + assert testmod.__spec__.origin == '/ram/testmod.py' + assert testmod.__file__ == '/ram/testmod.py' + + import testpkg + assert testpkg.var == 2 + assert testpkg.__package__ == 'testpkg' + assert testpkg.__path__ == ['/ram/testpkg'] + assert testpkg.__spec__.name == 'testpkg' + assert testpkg.__spec__.origin == '/ram/testpkg/__init__.py' + assert testpkg.__file__ == '/ram/testpkg/__init__.py' + + from testpkg import test + assert test.var == 3 + assert test.__spec__.name == 'testpkg.test' + assert test.__spec__.origin == '/ram/testpkg/test.py' + assert test.__file__ == '/ram/testpkg/test.py' + + from testnspkg import test + assert test.var == 4 + assert test.__spec__.name == 'testnspkg.test' + assert test.__spec__.origin == '/ram/testnspkg/test.py' + assert test.__file__ == '/ram/testnspkg/test.py' + + finally: + vfs.unmount(mount) + try: + del sys.path[sys.path.index("/ram")] + except ValueError: + pass From e4e5d75df7944c885c3b3eba4e2903429b137f89 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 22:47:16 +0200 Subject: [PATCH 097/291] tests: Fix mypy, Windows test failures --- tests/showbase/test_VFSImporter.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/showbase/test_VFSImporter.py b/tests/showbase/test_VFSImporter.py index 33c1c6a186..52ce6c3206 100644 --- a/tests/showbase/test_VFSImporter.py +++ b/tests/showbase/test_VFSImporter.py @@ -1,4 +1,4 @@ -from panda3d.core import VirtualFileSystem, VirtualFileMountRamdisk +from panda3d.core import VirtualFileSystem, VirtualFileMountRamdisk, Filename import sys @@ -25,28 +25,32 @@ def test_VFSImporter(): import testmod assert testmod.var == 1 assert testmod.__spec__.name == 'testmod' - assert testmod.__spec__.origin == '/ram/testmod.py' - assert testmod.__file__ == '/ram/testmod.py' + filename = Filename('/ram/testmod.py').to_os_specific() + assert testmod.__spec__.origin == filename + assert testmod.__file__ == filename import testpkg assert testpkg.var == 2 assert testpkg.__package__ == 'testpkg' - assert testpkg.__path__ == ['/ram/testpkg'] + assert testpkg.__path__ == [Filename('/ram/testpkg').to_os_specific()] assert testpkg.__spec__.name == 'testpkg' - assert testpkg.__spec__.origin == '/ram/testpkg/__init__.py' - assert testpkg.__file__ == '/ram/testpkg/__init__.py' + filename = Filename('/ram/testpkg/__init__.py').to_os_specific() + assert testpkg.__spec__.origin == filename + assert testpkg.__file__ == filename from testpkg import test assert test.var == 3 assert test.__spec__.name == 'testpkg.test' - assert test.__spec__.origin == '/ram/testpkg/test.py' - assert test.__file__ == '/ram/testpkg/test.py' + filename = Filename('/ram/testpkg/test.py').to_os_specific() + assert test.__spec__.origin == filename + assert test.__file__ == filename from testnspkg import test assert test.var == 4 assert test.__spec__.name == 'testnspkg.test' - assert test.__spec__.origin == '/ram/testnspkg/test.py' - assert test.__file__ == '/ram/testnspkg/test.py' + filename = Filename('/ram/testnspkg/test.py').to_os_specific() + assert test.__spec__.origin == filename + assert test.__file__ == filename finally: vfs.unmount(mount) From fe65114cd729437ae4ee07f112940ee274b17d50 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 28 Jul 2025 22:50:14 +0200 Subject: [PATCH 098/291] VFSImporter: Add ignore directive for mypy It's complaining about something in the Python source, nothing we can do about that --- direct/src/showbase/VFSImporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/direct/src/showbase/VFSImporter.py b/direct/src/showbase/VFSImporter.py index 3845a0444c..76964dfc77 100644 --- a/direct/src/showbase/VFSImporter.py +++ b/direct/src/showbase/VFSImporter.py @@ -144,7 +144,7 @@ class VFSLoader(Loader): raise OSError -class VFSSourceLoader(VFSLoader, SourceLoader): +class VFSSourceLoader(VFSLoader, SourceLoader): # type: ignore[misc] def get_source(self, fullname): if fullname is not None and self.name != fullname: raise ImportError From f00cec70e62e2ae2c5e692ac9b6e3d903fb0c76e Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Sat, 2 Aug 2025 08:42:58 -0600 Subject: [PATCH 099/291] showbase: Annotate `VFSImporter` functions (#1764) --- direct/src/showbase/VFSImporter.py | 45 +++++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/direct/src/showbase/VFSImporter.py b/direct/src/showbase/VFSImporter.py index 76964dfc77..3bfa2336f1 100644 --- a/direct/src/showbase/VFSImporter.py +++ b/direct/src/showbase/VFSImporter.py @@ -5,9 +5,11 @@ Calling the :func:`register()` function to register the import hooks should be sufficient to enable this functionality. """ +from __future__ import annotations + __all__ = ['register'] -from panda3d.core import Filename, VirtualFileSystem, VirtualFileMountSystem +from panda3d.core import Filename, VirtualFile, VirtualFileSystem, VirtualFileMountSystem from panda3d.core import OFileStream, copy_stream import sys import marshal @@ -16,14 +18,17 @@ import atexit from importlib.abc import Loader, SourceLoader from importlib.util import MAGIC_NUMBER, decode_source from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, BYTECODE_SUFFIXES +from types import ModuleType +from typing import Any vfs = VirtualFileSystem.get_global_ptr() -def _make_spec(fullname, loader, *, is_package): +def _make_spec(fullname: str, loader: VFSLoader, *, is_package: bool) -> ModuleSpec: filename = loader._vfile.get_filename() spec = ModuleSpec(fullname, loader, origin=filename.to_os_specific(), is_package=is_package) if is_package: + assert spec.submodule_search_locations is not None spec.submodule_search_locations.append(Filename(filename.get_dirname()).to_os_specific()) spec.has_location = True return spec @@ -35,16 +40,15 @@ class VFSFinder: which allows loading Python source files from mounted .mf files (among other places). """ - def __init__(self, path): + def __init__(self, path: str) -> None: self.path = path - def find_spec(self, fullname, path, target=None): - if path is None: - path = self.path - + def find_spec(self, fullname: str, target: ModuleType | None = None) -> ModuleSpec | None: #print(f"find_spec({fullname}), dir_path = {dir_path}", file=sys.stderr) basename = fullname.split('.')[-1] - filename = Filename(Filename.from_os_specific(path), basename) + filename = Filename(Filename.from_os_specific(self.path), basename) + + loader: VFSLoader # First, look for Python files. vfile = vfs.get_file(filename + '.py', True) @@ -84,6 +88,7 @@ class VFSFinder: # Consider a namespace package. if vfs.is_directory(filename): spec = ModuleSpec(fullname, VFSNamespaceLoader(), is_package=True) + assert spec.submodule_search_locations is not None spec.submodule_search_locations.append(filename.to_os_specific()) return spec @@ -92,7 +97,7 @@ class VFSFinder: class VFSLoader(Loader): - def __init__(self, fullname, vfile): + def __init__(self, fullname: str, vfile: VirtualFile) -> None: self.name = fullname self._vfile = vfile @@ -105,22 +110,22 @@ class VFSLoader(Loader): tail_name = fullname.rpartition('.')[2] return filename_base == '__init__' and tail_name != '__init__' - def create_module(self, spec): + def create_module(self, spec: ModuleSpec) -> ModuleType | None: """Use default semantics for module creation.""" - def exec_module(self, module): + def exec_module(self, module: ModuleType) -> None: """Execute the module.""" - code = self.get_code(module.__name__) + code = self.get_code(module.__name__) # type: ignore[attr-defined] exec(code, module.__dict__) - def get_filename(self, fullname): + def get_filename(self, fullname: str) -> str: if fullname is not None and self.name != fullname: raise ImportError return self._vfile.get_filename().to_os_specific() @staticmethod - def get_data(path): + def get_data(path: str) -> bytes: vfile = vfs.get_file(Filename.from_os_specific(path)) if vfile: return vfile.read_file(True) @@ -128,7 +133,7 @@ class VFSLoader(Loader): raise OSError @staticmethod - def path_stats(path): + def path_stats(path: str) -> dict[str, Any]: vfile = vfs.get_file(Filename.from_os_specific(path)) if vfile: return {'mtime': vfile.get_timestamp(), 'size': vfile.get_file_size()} @@ -231,11 +236,11 @@ class VFSExtensionLoader(VFSLoader): return None -class VFSNamespaceLoader: - def create_module(self, spec): +class VFSNamespaceLoader(Loader): + def create_module(self, spec: ModuleSpec) -> ModuleType | None: """Use default semantics for module creation.""" - def exec_module(self, module): + def exec_module(self, module: ModuleType) -> None: pass def is_package(self, fullname): @@ -248,7 +253,7 @@ class VFSNamespaceLoader: return compile('', '', 'exec', dont_inherit=True) -def _path_hook(entry): +def _path_hook(entry: str) -> VFSFinder: # If this is a directory in the VFS, create a VFSFinder for this entry. vfile = vfs.get_file(Filename.from_os_specific(entry), False) if vfile and vfile.is_directory() and not isinstance(vfile.get_mount(), VirtualFileMountSystem): @@ -259,7 +264,7 @@ def _path_hook(entry): _registered = False -def register(): +def register() -> None: """ Register the VFSFinder on the path_hooks, if it has not already been registered, so that future Python import statements will vector through here (and therefore will take advantage of From 0e1a3f2ab7f2b48a50ef423fa192a9e7c9568f39 Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Sat, 2 Aug 2025 08:43:50 -0600 Subject: [PATCH 100/291] stdpy: Annotate threading functions (#1765) --- direct/src/stdpy/thread.py | 30 +++++++---- direct/src/stdpy/threading.py | 46 +++++++++++----- direct/src/stdpy/threading2.py | 98 +++++++++++++++++++--------------- 3 files changed, 107 insertions(+), 67 deletions(-) diff --git a/direct/src/stdpy/thread.py b/direct/src/stdpy/thread.py index 69240721bb..7439ba5492 100644 --- a/direct/src/stdpy/thread.py +++ b/direct/src/stdpy/thread.py @@ -5,6 +5,8 @@ in some compilation models, Panda's threading constructs are incompatible with the OS-provided threads used by Python's thread module. """ +from __future__ import annotations + __all__ = [ 'error', 'LockType', 'start_new_thread', @@ -19,6 +21,9 @@ __all__ = [ from panda3d import core import sys +from collections.abc import Callable, Iterable, Mapping +from typing import Any + if sys.platform == "win32": TIMEOUT_MAX = float(0xffffffff // 1000) else: @@ -41,12 +46,12 @@ class LockType: allows a different thread to release the lock than the one that acquired it. """ - def __init__(self): + def __init__(self) -> None: self.__lock = core.Mutex('PythonLock') self.__cvar = core.ConditionVar(self.__lock) self.__locked = False - def acquire(self, waitflag = 1, timeout = -1): + def acquire(self, waitflag: bool = True, timeout: float = -1) -> bool: self.__lock.acquire() try: if self.__locked and not waitflag: @@ -65,7 +70,7 @@ class LockType: finally: self.__lock.release() - def release(self): + def release(self) -> None: self.__lock.acquire() try: if not self.__locked: @@ -90,18 +95,23 @@ class LockType: _counter = 0 -def _newname(template="Thread-%d"): +def _newname(template: str = "Thread-%d") -> str: global _counter _counter = _counter + 1 return template % _counter -_threads = {} +_threads: dict[int, tuple[core.Thread, dict[int, dict[str, Any]], Any | None]] = {} _nextThreadId = 0 _threadsLock = core.Mutex('thread._threadsLock') -def start_new_thread(function, args, kwargs = {}, name = None): +def start_new_thread( + function: Callable[..., object], + args: Iterable[Any], + kwargs: Mapping[str, Any] = {}, + name: str | None = None, +) -> int: def threadFunc(threadId, function = function, args = args, kwargs = kwargs): try: try: @@ -132,7 +142,7 @@ def start_new_thread(function, args, kwargs = {}, name = None): _threadsLock.release() -def _add_thread(thread, wrapper): +def _add_thread(thread: core.Thread, wrapper: Any) -> int: """ Adds the indicated core.Thread object, with the indicated Python wrapper, to the thread list. Returns the new thread ID. """ @@ -150,7 +160,7 @@ def _add_thread(thread, wrapper): _threadsLock.release() -def _get_thread_wrapper(thread, wrapperClass): +def _get_thread_wrapper(thread: core.Thread, wrapperClass: Callable[[core.Thread, int], Any]) -> Any: """ Returns the thread wrapper for the indicated thread. If there is not one, creates an instance of the indicated wrapperClass instead. """ @@ -222,7 +232,7 @@ def _get_thread_locals(thread, i): _threadsLock.release() -def _remove_thread_id(threadId): +def _remove_thread_id(threadId: int) -> None: """ Removes the thread with the indicated ID from the thread list. """ # On interpreter shutdown, Python may set module globals to None. @@ -254,7 +264,7 @@ def allocate_lock() -> LockType: return LockType() -def get_ident(): +def get_ident() -> int: return core.Thread.getCurrentThread().this diff --git a/direct/src/stdpy/threading.py b/direct/src/stdpy/threading.py index 3b82b61466..e34b24342c 100644 --- a/direct/src/stdpy/threading.py +++ b/direct/src/stdpy/threading.py @@ -21,12 +21,17 @@ easier to use and understand. It is permissible to mix-and-match both threading and threading2 within the same application. """ +from __future__ import annotations + from panda3d import core from direct.stdpy import thread as _thread import sys as _sys import weakref +from collections.abc import Callable, Iterable, Mapping +from typing import Any, NoReturn + __all__ = [ 'Thread', 'Lock', 'RLock', @@ -54,10 +59,14 @@ class ThreadBase: """ A base class for both Thread and ExternalThread in this module. """ - def __init__(self): + name: str + ident: int + daemon: bool + + def __init__(self) -> None: pass - def getName(self): + def getName(self) -> str: return self.name def isDaemon(self): @@ -92,7 +101,15 @@ class Thread(ThreadBase): object. The wrapper is designed to emulate Python's own threading.Thread object. """ - def __init__(self, group=None, target=None, name=None, args=(), kwargs={}, daemon=None): + def __init__( + self, + group: None = None, + target: Callable[..., object] | None = None, + name: str | None = None, + args: Iterable[Any] = (), + kwargs: Mapping[str, Any] = {}, + daemon: bool | None = None, + ) -> None: ThreadBase.__init__(self) assert group is None @@ -131,7 +148,7 @@ class Thread(ThreadBase): isAlive = is_alive - def start(self): + def start(self) -> None: thread = self.__thread if thread is None or thread.is_started(): raise RuntimeError @@ -147,7 +164,7 @@ class Thread(ThreadBase): self.__target(*self.__args, **self.__kwargs) - def join(self, timeout = None): + def join(self, timeout: float | None = None) -> None: # We don't support a timed join here, sorry. assert timeout is None thread = self.__thread @@ -157,7 +174,7 @@ class Thread(ThreadBase): self.__thread = None _thread._remove_thread_id(self.ident) - def setName(self, name): + def setName(self, name: str) -> None: self.__dict__['name'] = name self.__thread.setName(name) @@ -166,7 +183,7 @@ class ExternalThread(ThreadBase): """ Returned for a Thread object that wasn't created by this interface. """ - def __init__(self, extThread, threadId): + def __init__(self, extThread: core.Thread, threadId: int) -> None: ThreadBase.__init__(self) self.__thread = extThread @@ -196,7 +213,7 @@ class ExternalThread(ThreadBase): class MainThread(ExternalThread): """ Returned for the MainThread object. """ - def __init__(self, extThread, threadId): + def __init__(self, extThread: core.Thread, threadId: int) -> None: ExternalThread.__init__(self, extThread, threadId) self.__dict__['daemon'] = False @@ -206,7 +223,7 @@ class Lock(core.Mutex): The wrapper is designed to emulate Python's own threading.Lock object. """ - def __init__(self, name = "PythonLock"): + def __init__(self, name: str = "PythonLock") -> None: core.Mutex.__init__(self, name) @@ -224,7 +241,7 @@ class Condition(core.ConditionVar): object. The wrapper is designed to emulate Python's own threading.Condition object. """ - def __init__(self, lock = None): + def __init__(self, lock: Lock | RLock | None = None) -> None: if not lock: lock = Lock() @@ -241,7 +258,7 @@ class Condition(core.ConditionVar): def release(self): self.__lock.release() - def wait(self, timeout = None): + def wait(self, timeout: float | None = None) -> None: if timeout is None: core.ConditionVar.wait(self) else: @@ -373,8 +390,9 @@ class Timer(Thread): self.finished.set() -def _create_thread_wrapper(t, threadId): +def _create_thread_wrapper(t: core.Thread, threadId: int) -> ExternalThread: """ Creates a thread wrapper for the indicated external thread. """ + pyt: ExternalThread if isinstance(t, core.MainThread): pyt = MainThread(t, threadId) else: @@ -383,7 +401,7 @@ def _create_thread_wrapper(t, threadId): return pyt -def current_thread(): +def current_thread() -> ThreadBase: t = core.Thread.getCurrentThread() return _thread._get_thread_wrapper(t, _create_thread_wrapper) @@ -430,5 +448,5 @@ def setprofile(func): _setprofile_func = func -def stack_size(size = None): +def stack_size(size: object = None) -> NoReturn: raise ThreadError diff --git a/direct/src/stdpy/threading2.py b/direct/src/stdpy/threading2.py index b2d49cdc90..cde6e0aa7e 100644 --- a/direct/src/stdpy/threading2.py +++ b/direct/src/stdpy/threading2.py @@ -13,6 +13,8 @@ to import Panda's thread reimplementation instead of the system thread module, and so it is therefore layered on top of Panda's thread implementation. """ +from __future__ import annotations + import sys as _sys import atexit as _atexit @@ -21,8 +23,10 @@ from direct.stdpy.thread import stack_size, _newname, _local as local from panda3d import core _sleep = core.Thread.sleep +from collections.abc import Callable, Iterable, Mapping from time import time as _time from traceback import format_exc as _format_exc +from typing import Any __all__ = ['get_ident', 'active_count', 'Condition', 'current_thread', 'enumerate', 'main_thread', 'TIMEOUT_MAX', @@ -51,12 +55,12 @@ if __debug__: class _Verbose(object): - def __init__(self, verbose=None): + def __init__(self, verbose: bool | None = None) -> None: if verbose is None: verbose = _VERBOSE self.__verbose = verbose - def _note(self, format, *args): + def _note(self, format: str, *args: Any) -> None: if self.__verbose: format = format % args format = "%s: %s\n" % ( @@ -66,9 +70,9 @@ if __debug__: else: # Disable this when using "python -O" class _Verbose(object): # type: ignore[no-redef] - def __init__(self, verbose=None): + def __init__(self, verbose: bool | None = None) -> None: pass - def _note(self, *args): + def _note(self, *args) -> None: pass # Support for profile and trace hooks @@ -88,15 +92,15 @@ def settrace(func): Lock = _allocate_lock -def RLock(*args, **kwargs): - return _RLock(*args, **kwargs) +def RLock(verbose: bool | None = None) -> _RLock: + return _RLock(verbose) class _RLock(_Verbose): - def __init__(self, verbose=None): + def __init__(self, verbose: bool | None = None) -> None: _Verbose.__init__(self, verbose) self.__block = _allocate_lock() - self.__owner = None + self.__owner: Thread | None = None self.__count = 0 def __repr__(self): @@ -105,13 +109,13 @@ class _RLock(_Verbose): self.__owner and self.__owner.getName(), self.__count) - def acquire(self, blocking=1): + def acquire(self, blocking: bool = True) -> bool: me = currentThread() if self.__owner is me: self.__count = self.__count + 1 if __debug__: self._note("%s.acquire(%s): recursive success", self, blocking) - return 1 + return True rc = self.__block.acquire(blocking) if rc: self.__owner = me @@ -125,7 +129,7 @@ class _RLock(_Verbose): __enter__ = acquire - def release(self): + def release(self) -> None: me = currentThread() assert self.__owner is me, "release() of un-acquire()d lock" self.__count = count = self.__count - 1 @@ -163,12 +167,12 @@ class _RLock(_Verbose): return self.__owner is currentThread() -def Condition(*args, **kwargs): - return _Condition(*args, **kwargs) +def Condition(lock: _thread.LockType | _RLock | None = None, verbose: bool | None = None) -> _Condition: + return _Condition(lock, verbose) class _Condition(_Verbose): - def __init__(self, lock=None, verbose=None): + def __init__(self, lock: _thread.LockType | _RLock | None = None, verbose: bool | None = None) -> None: _Verbose.__init__(self, verbose) if lock is None: lock = RLock() @@ -180,18 +184,18 @@ class _Condition(_Verbose): # these override the default implementations (which just call # release() and acquire() on the lock). Ditto for _is_owned(). try: - self._release_save = lock._release_save + self._release_save = lock._release_save # type: ignore[method-assign, union-attr] except AttributeError: pass try: - self._acquire_restore = lock._acquire_restore + self._acquire_restore = lock._acquire_restore # type: ignore[method-assign, union-attr] except AttributeError: pass try: - self._is_owned = lock._is_owned + self._is_owned = lock._is_owned # type: ignore[method-assign, union-attr] except AttributeError: pass - self.__waiters = [] + self.__waiters: list[_thread.LockType] = [] def __enter__(self): return self.__lock.__enter__() @@ -202,22 +206,22 @@ class _Condition(_Verbose): def __repr__(self): return "" % (self.__lock, len(self.__waiters)) - def _release_save(self): # pylint: disable=method-hidden + def _release_save(self) -> Any: # pylint: disable=method-hidden self.__lock.release() # No state to save - def _acquire_restore(self, x): # pylint: disable=method-hidden + def _acquire_restore(self, x) -> None: # pylint: disable=method-hidden self.__lock.acquire() # Ignore saved state - def _is_owned(self): # pylint: disable=method-hidden + def _is_owned(self) -> bool: # pylint: disable=method-hidden # Return True if lock is owned by currentThread. # This method is called only if __lock doesn't have _is_owned(). - if self.__lock.acquire(0): + if self.__lock.acquire(False): self.__lock.release() return False else: return True - def wait(self, timeout=None): + def wait(self, timeout: float | None = None) -> None: assert self._is_owned(), "wait() of un-acquire()d lock" waiter = _allocate_lock() waiter.acquire() @@ -237,7 +241,7 @@ class _Condition(_Verbose): endtime = _time() + timeout delay = 0.0005 # 500 us -> initial delay of 1 ms while True: - gotit = waiter.acquire(0) + gotit = waiter.acquire(False) if gotit: break remaining = endtime - _time() @@ -258,7 +262,7 @@ class _Condition(_Verbose): finally: self._acquire_restore(saved_state) - def notify(self, n=1): + def notify(self, n: int = 1) -> None: assert self._is_owned(), "notify() of un-acquire()d lock" __waiters = self.__waiters waiters = __waiters[:n] @@ -275,7 +279,7 @@ class _Condition(_Verbose): except ValueError: pass - def notifyAll(self): + def notifyAll(self) -> None: self.notify(len(self.__waiters)) @@ -381,7 +385,7 @@ class _Event(_Verbose): # Active thread administration _active_limbo_lock = _allocate_lock() -_active = {} # maps thread id to Thread object +_active: dict[int, Thread] = {} # maps thread id to Thread object _limbo = {} @@ -400,8 +404,16 @@ class Thread(_Verbose): # Protected by _active_limbo_lock. __registered_atexit = False - def __init__(self, group=None, target=None, name=None, - args=(), kwargs=None, verbose=None, daemon=None): + def __init__( + self, + group: None = None, + target: Callable[..., object] | None = None, + name: object = None, + args: Iterable[Any] = (), + kwargs: Mapping[str, Any] | None = None, + verbose: bool | None = None, + daemon: bool | None = None, + ) -> None: assert group is None, "group argument must be None for now" _Verbose.__init__(self, verbose) if kwargs is None: @@ -422,7 +434,7 @@ class Thread(_Verbose): # sys.exc_info since it can be changed between instances self.__stderr = _sys.stderr - def _set_daemon(self): + def _set_daemon(self) -> bool: # Overridden in _MainThread and _DummyThread return currentThread().isDaemon() @@ -437,7 +449,7 @@ class Thread(_Verbose): status = status + " daemon" return "<%s(%s, %s)>" % (self.__class__.__name__, self.__name, status) - def start(self): + def start(self) -> None: assert self.__initialized, "Thread.__init__() not called" assert not self.__started, "thread already started" if __debug__: @@ -457,11 +469,11 @@ class Thread(_Verbose): self.__started = True _sleep(0.000001) # 1 usec, to let the thread run (Solaris hack) - def run(self): + def run(self) -> None: if self.__target: self.__target(*self.__args, **self.__kwargs) - def __bootstrap(self): + def __bootstrap(self) -> None: try: self.__started = True _active_limbo_lock.acquire() @@ -497,7 +509,7 @@ class Thread(_Verbose): # Do the best job possible w/o a huge amt. of code to # approximate a traceback (code ideas from # Lib/traceback.py) - exc_type, exc_value, exc_tb = self.__exc_info() + exc_type, exc_value, exc_tb = self.__exc_info() # type: ignore[misc] try: self.__stderr.write("Exception in thread " + self.getName() + " (most likely raised during interpreter shutdown):\n") @@ -523,13 +535,13 @@ class Thread(_Verbose): except: pass - def __stop(self): + def __stop(self) -> None: self.__block.acquire() self.__stopped = True self.__block.notifyAll() self.__block.release() - def __delete(self): + def __delete(self) -> None: "Remove current thread from the dict of currently running threads." # Notes about running with dummy_thread: @@ -563,7 +575,7 @@ class Thread(_Verbose): finally: _active_limbo_lock.release() - def join(self, timeout=None): + def join(self, timeout: float | None = None) -> None: assert self.__initialized, "Thread.__init__() not called" assert self.__started, "cannot join thread before it is started" assert self is not currentThread(), "cannot join current thread" @@ -592,11 +604,11 @@ class Thread(_Verbose): finally: self.__block.release() - def getName(self): + def getName(self) -> str: assert self.__initialized, "Thread.__init__() not called" return self.__name - def setName(self, name): + def setName(self, name: object) -> None: assert self.__initialized, "Thread.__init__() not called" self.__name = str(name) @@ -606,7 +618,7 @@ class Thread(_Verbose): isAlive = is_alive - def isDaemon(self): + def isDaemon(self) -> bool: assert self.__initialized, "Thread.__init__() not called" return self.__daemonic @@ -688,13 +700,13 @@ class _MainThread(Thread): class _DummyThread(Thread): - def __init__(self): + def __init__(self) -> None: Thread.__init__(self, name=_newname("Dummy-%d"), daemon=True) # Thread.__block consumes an OS-level locking primitive, which # can never be used by a _DummyThread. Since a _DummyThread # instance is immortal, that's bad, so release this resource. - del self._Thread__block + del self._Thread__block # type: ignore[attr-defined] self._Thread__started = True _active_limbo_lock.acquire() @@ -710,7 +722,7 @@ class _DummyThread(Thread): # Global API functions -def current_thread(): +def current_thread() -> Thread: try: return _active[get_ident()] except KeyError: From 3564f2df5307f38d51f49aa2d7d55950b1f56a5a Mon Sep 17 00:00:00 2001 From: chandini sri mounika vissamsetti Date: Sat, 2 Aug 2025 20:14:42 +0530 Subject: [PATCH 101/291] readme: Improve bug reporting section with links (#1761) [skip ci] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 881b30fc5b..1a61d39d87 100644 --- a/README.md +++ b/README.md @@ -228,8 +228,8 @@ information as possible to help the developers track down the issue, such as your version of Panda3D, operating system, architecture, and any code and models that are necessary for the developers to reproduce the issue. -If you're not sure whether you've encountered a bug, feel free to ask about -it in the forums or the IRC channel first. +If you're unsure whether you've encountered a bug, feel free to ask in the [forums](https://discourse.panda3d.org) or the [IRC channel](https://web.libera.chat/#panda3d) before opening an issue. + Supporting the Project ====================== From 052bd770300f407b11a428f3cfb74e24515abdd1 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 3 Aug 2025 11:54:46 +0200 Subject: [PATCH 102/291] loader: Remember type of loader object used to load models Fixes #1657 --- panda/src/pgraph/loader.cxx | 46 +++++++++++++++++++++++++------- panda/src/pgraph/modelRoot.I | 26 +++++++++++++++--- panda/src/pgraph/modelRoot.cxx | 10 +++++++ panda/src/pgraph/modelRoot.h | 8 ++++++ panda/src/putil/bam.h | 5 ++-- panda/src/putil/bamCacheRecord.I | 1 + panda/src/putil/bamCacheRecord.h | 1 + 7 files changed, 82 insertions(+), 15 deletions(-) diff --git a/panda/src/pgraph/loader.cxx b/panda/src/pgraph/loader.cxx index a051aeaf35..3d9067e053 100644 --- a/panda/src/pgraph/loader.cxx +++ b/panda/src/pgraph/loader.cxx @@ -305,21 +305,44 @@ try_load_file(const Filename &pathname, const LoaderOptions &options, (options.get_flags() & LoaderOptions::LF_no_disk_cache) == 0) { // See if the model can be found in the on-disk cache, if it is active. record = cache->lookup(pathname, "bam"); - if (record != nullptr) { - if (record->has_data()) { - if (report_errors) { - loader_cat.info() - << "Model " << pathname << " found in disk cache.\n"; + if (record != nullptr && record->has_data()) { + PT(PandaNode) result = DCAST(PandaNode, record->get_data()); + + ModelRoot *model_root = nullptr; + if (result != nullptr && result->is_of_type(ModelRoot::get_class_type())) { + model_root = DCAST(ModelRoot, result.p()); + + if (requested_type != nullptr && + model_root->get_loader_type() != requested_type->get_type()) { + result.clear(); + if (report_errors) { + loader_cat.info() + << "Model " << pathname << " found in disk cache, but using " + << "unexpected loader " << model_root->get_loader_type() + << " (expected " << requested_type->get_type() << ").\n"; + } + } + } + + if (result != nullptr) { + if (report_errors) { + TypeHandle loaded_type = model_root->get_loader_type(); + if (loaded_type != TypeHandle::none()) { + loader_cat.info() + << "Model " << pathname << " found in disk cache (loaded using " + << loaded_type << ").\n"; + } else { + loader_cat.info() + << "Model " << pathname << " found in disk cache.\n"; + } } - PT(PandaNode) result = DCAST(PandaNode, record->get_data()); if (premunge_data) { SceneGraphReducer sgr; sgr.premunge(result, RenderState::make_empty()); } - if (result->is_of_type(ModelRoot::get_class_type())) { - ModelRoot *model_root = DCAST(ModelRoot, result.p()); + if (model_root != nullptr) { model_root->set_fullpath(pathname); model_root->set_timestamp(record->get_source_timestamp()); @@ -336,8 +359,7 @@ try_load_file(const Filename &pathname, const LoaderOptions &options, return result; } } - - if (loader_cat.is_debug()) { + else if (loader_cat.is_debug()) { loader_cat.debug() << "Model " << pathname << " not found in cache.\n"; } @@ -354,6 +376,10 @@ try_load_file(const Filename &pathname, const LoaderOptions &options, result = requested_type->load_file(pathname, options, record); } if (result != nullptr) { + if (result->is_of_type(ModelRoot::get_class_type())) { + ((ModelRoot *)result.p())->set_loader_type(requested_type->get_type()); + } + if (record != nullptr) { // Store the loaded model in the model cache. record->set_data(result); diff --git a/panda/src/pgraph/modelRoot.I b/panda/src/pgraph/modelRoot.I index 4478702a28..7a8ea482ff 100644 --- a/panda/src/pgraph/modelRoot.I +++ b/panda/src/pgraph/modelRoot.I @@ -19,7 +19,8 @@ ModelRoot(const std::string &name) : ModelNode(name), _fullpath(name), _timestamp(0), - _reference(new ModelRoot::ModelReference) + _reference(new ModelRoot::ModelReference), + _loader_type(TypeHandle::none()) { } @@ -31,7 +32,8 @@ ModelRoot(const Filename &fullpath, time_t timestamp) : ModelNode(fullpath.get_basename()), _fullpath(fullpath), _timestamp(timestamp), - _reference(new ModelRoot::ModelReference) + _reference(new ModelRoot::ModelReference), + _loader_type(TypeHandle::none()) { } @@ -114,6 +116,23 @@ set_reference(ModelRoot::ModelReference *ref) { _reference = ref; } +/** + * Returns the type of the loader object that was used to load this model. + */ +INLINE TypeHandle ModelRoot:: +get_loader_type() const { + return _loader_type; +} + +/** + * Sets the type of the loader object used to load this model. Normally this + * is only called by Loader. + */ +INLINE void ModelRoot:: +set_loader_type(TypeHandle type) { + _loader_type = type; +} + /** * */ @@ -122,7 +141,8 @@ ModelRoot(const ModelRoot ©) : ModelNode(copy), _fullpath(copy._fullpath), _timestamp(copy._timestamp), - _reference(copy._reference) + _reference(copy._reference), + _loader_type(copy._loader_type) { } diff --git a/panda/src/pgraph/modelRoot.cxx b/panda/src/pgraph/modelRoot.cxx index 30dbf1e6a6..e39d101c2e 100644 --- a/panda/src/pgraph/modelRoot.cxx +++ b/panda/src/pgraph/modelRoot.cxx @@ -41,6 +41,10 @@ register_with_read_factory() { void ModelRoot:: write_datagram(BamWriter *manager, Datagram &dg) { ModelNode::write_datagram(manager, dg); + + if (manager->get_file_minor_ver() >= 46) { + manager->write_handle(dg, _loader_type); + } } /** @@ -67,4 +71,10 @@ make_from_bam(const FactoryParams ¶ms) { void ModelRoot:: fillin(DatagramIterator &scan, BamReader *manager) { ModelNode::fillin(scan, manager); + + if (manager->get_file_minor_ver() >= 46) { + _loader_type = manager->read_handle(scan); + } else { + _loader_type = TypeHandle::none(); + } } diff --git a/panda/src/pgraph/modelRoot.h b/panda/src/pgraph/modelRoot.h index c73363c108..d249340b11 100644 --- a/panda/src/pgraph/modelRoot.h +++ b/panda/src/pgraph/modelRoot.h @@ -50,6 +50,13 @@ PUBLISHED: void set_reference(ModelReference *ref); MAKE_PROPERTY(reference, get_reference, set_reference); +public: + INLINE TypeHandle get_loader_type() const; + INLINE void set_loader_type(TypeHandle loader_type); + +PUBLISHED: + MAKE_PROPERTY(loader_type, get_loader_type); + protected: INLINE ModelRoot(const ModelRoot ©); @@ -60,6 +67,7 @@ private: Filename _fullpath; time_t _timestamp; PT(ModelReference) _reference; + TypeHandle _loader_type; public: static void register_with_read_factory(); diff --git a/panda/src/putil/bam.h b/panda/src/putil/bam.h index 33ce0b9f38..7647ab0f87 100644 --- a/panda/src/putil/bam.h +++ b/panda/src/putil/bam.h @@ -32,8 +32,8 @@ static const unsigned short _bam_major_ver = 6; // Bumped to major version 6 on 2006-02-11 to factor out PandaNode::CData. static const unsigned short _bam_first_minor_ver = 14; -static const unsigned short _bam_last_minor_ver = 45; -static const unsigned short _bam_minor_ver = 44; +static const unsigned short _bam_last_minor_ver = 46; +static const unsigned short _bam_minor_ver = 46; // Bumped to minor version 14 on 2007-12-19 to change default ColorAttrib. // Bumped to minor version 15 on 2008-04-09 to add TextureAttrib::_implicit_sort. // Bumped to minor version 16 on 2008-05-13 to add Texture::_quality_level. @@ -66,5 +66,6 @@ static const unsigned short _bam_minor_ver = 44; // Bumped to minor version 43 on 2018-12-06 to expand BillboardEffect and CompassEffect. // Bumped to minor version 44 on 2018-12-23 to rename CollisionTube to CollisionCapsule. // Bumped to minor version 45 on 2020-03-18 to add Texture::_clear_color. +// Bumped to minor version 46 on 2025-08-03 to add ModelRoot::_loader_type. #endif diff --git a/panda/src/putil/bamCacheRecord.I b/panda/src/putil/bamCacheRecord.I index e5288c03ac..f2fb5aa0e1 100644 --- a/panda/src/putil/bamCacheRecord.I +++ b/panda/src/putil/bamCacheRecord.I @@ -30,6 +30,7 @@ operator == (const BamCacheRecord &other) const { return (_source_pathname == other._source_pathname && _cache_filename == other._cache_filename && _recorded_time == other._recorded_time && + _loader_type == other._loader_type && _record_size == other._record_size); } diff --git a/panda/src/putil/bamCacheRecord.h b/panda/src/putil/bamCacheRecord.h index 39b3728dfa..2f904f8db5 100644 --- a/panda/src/putil/bamCacheRecord.h +++ b/panda/src/putil/bamCacheRecord.h @@ -93,6 +93,7 @@ private: time_t _recorded_time; std::streamsize _record_size; // this is accurate only in the index file. time_t _source_timestamp; // Not record to the cache file. + TypeHandle _loader_type; class DependentFile { public: From 66c9264f300fe41f37a30c4ffbde8ba2a9fb8097 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 3 Aug 2025 11:59:28 +0200 Subject: [PATCH 103/291] bam: Add forward compatibility for bam 6.46 See #1657 and 052bd770300f407b11a428f3cfb74e24515abdd1 --- panda/src/pgraph/modelRoot.cxx | 8 ++++++++ panda/src/putil/bam.h | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/panda/src/pgraph/modelRoot.cxx b/panda/src/pgraph/modelRoot.cxx index 30dbf1e6a6..aae68b052e 100644 --- a/panda/src/pgraph/modelRoot.cxx +++ b/panda/src/pgraph/modelRoot.cxx @@ -41,6 +41,10 @@ register_with_read_factory() { void ModelRoot:: write_datagram(BamWriter *manager, Datagram &dg) { ModelNode::write_datagram(manager, dg); + + if (manager->get_file_minor_ver() >= 46) { + manager->write_handle(dg, TypeHandle::none()); + } } /** @@ -67,4 +71,8 @@ make_from_bam(const FactoryParams ¶ms) { void ModelRoot:: fillin(DatagramIterator &scan, BamReader *manager) { ModelNode::fillin(scan, manager); + + if (manager->get_file_minor_ver() >= 46) { + manager->read_handle(scan); + } } diff --git a/panda/src/putil/bam.h b/panda/src/putil/bam.h index 33ce0b9f38..31008f788c 100644 --- a/panda/src/putil/bam.h +++ b/panda/src/putil/bam.h @@ -32,7 +32,7 @@ static const unsigned short _bam_major_ver = 6; // Bumped to major version 6 on 2006-02-11 to factor out PandaNode::CData. static const unsigned short _bam_first_minor_ver = 14; -static const unsigned short _bam_last_minor_ver = 45; +static const unsigned short _bam_last_minor_ver = 46; static const unsigned short _bam_minor_ver = 44; // Bumped to minor version 14 on 2007-12-19 to change default ColorAttrib. // Bumped to minor version 15 on 2008-04-09 to add TextureAttrib::_implicit_sort. @@ -66,5 +66,6 @@ static const unsigned short _bam_minor_ver = 44; // Bumped to minor version 43 on 2018-12-06 to expand BillboardEffect and CompassEffect. // Bumped to minor version 44 on 2018-12-23 to rename CollisionTube to CollisionCapsule. // Bumped to minor version 45 on 2020-03-18 to add Texture::_clear_color. +// Bumped to minor version 46 on 2025-08-03 to add ModelRoot::_loader_type. #endif From be2ff1563406afb3cb6a68edc8ea09d2c7ef7549 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 07:18:36 +0000 Subject: [PATCH 104/291] build(deps): bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 6 +++--- .github/workflows/mypy.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88c8ef6308..5b1e447057 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 10 @@ -375,7 +375,7 @@ jobs: os: [ubuntu-22.04, windows-2022, macOS-13] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install dependencies (Ubuntu) if: matrix.os == 'ubuntu-22.04' run: | @@ -498,7 +498,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install dependencies run: | diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 377ab8ffa0..40e4cdcf46 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -10,7 +10,7 @@ jobs: python-version: ['3.9', '3.13'] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: From 67e3ccada75ea21599c1082d010b39abaf604baf Mon Sep 17 00:00:00 2001 From: WMOkiishi Date: Sat, 23 Aug 2025 02:50:17 -0600 Subject: [PATCH 105/291] showbase: Annotate some debug functions (#1767) --- direct/src/directtools/DirectUtil.py | 4 +-- direct/src/showbase/BufferViewer.py | 42 ++++++++++++++++--------- direct/src/showbase/ExceptionVarDump.py | 8 ++--- direct/src/showbase/GarbageReport.py | 41 +++++++++++++++++------- direct/src/showbase/Messenger.py | 2 +- direct/src/showbase/OnScreenDebug.py | 20 +++++++----- direct/src/showbase/PythonUtil.py | 27 ++++++++++------ direct/src/showbase/ShowBase.py | 6 ++-- 8 files changed, 97 insertions(+), 53 deletions(-) diff --git a/direct/src/directtools/DirectUtil.py b/direct/src/directtools/DirectUtil.py index 134832973d..7017f7b44a 100644 --- a/direct/src/directtools/DirectUtil.py +++ b/direct/src/directtools/DirectUtil.py @@ -1,4 +1,4 @@ -from panda3d.core import VBase4 +from panda3d.core import NodePath, VBase4 from direct.task.Task import Task from direct.task.TaskManagerGlobal import taskMgr @@ -55,7 +55,7 @@ def lerpBackgroundColor(r, g, b, duration): # Set direct drawing style for an object # Never light object or draw in wireframe -def useDirectRenderStyle(nodePath, priority = 0): +def useDirectRenderStyle(nodePath: NodePath, priority: int = 0) -> None: """ Function to force a node path to use direct render style: no lighting, and no wireframe diff --git a/direct/src/showbase/BufferViewer.py b/direct/src/showbase/BufferViewer.py index 4a7babcbc2..9afdbccdb7 100644 --- a/direct/src/showbase/BufferViewer.py +++ b/direct/src/showbase/BufferViewer.py @@ -12,6 +12,8 @@ Or, you can enable the following variable in your Config.prc:: show-buffers true """ +from __future__ import annotations + __all__ = ['BufferViewer'] from panda3d.core import ( @@ -39,14 +41,19 @@ from direct.task.TaskManagerGlobal import taskMgr from direct.directnotify.DirectNotifyGlobal import directNotify from direct.showbase.DirectObject import DirectObject import math +from typing import Literal, Union + +# The following variables are typing constructs used in annotations +# to succinctly express complex type structures. +_Texture = Union[Texture, GraphicsOutput, Literal['all']] class BufferViewer(DirectObject): notify = directNotify.newCategory('BufferViewer') - def __init__(self, win, parent): + def __init__(self, win: GraphicsOutput | None, parent: NodePath) -> None: """Access: private. Constructor.""" - self.enabled = 0 + self.enabled = False size = ConfigVariableDouble('buffer-viewer-size', '0 0') self.sizex = size[0] self.sizey = size[1] @@ -59,23 +66,23 @@ class BufferViewer(DirectObject): self.win = win self.engine = GraphicsEngine.getGlobalPtr() self.renderParent = parent - self.cards = [] + self.cards: list[NodePath] = [] self.cardindex = 0 self.cardmaker = CardMaker("cubemaker") self.cardmaker.setFrame(-1,1,-1,1) self.task = 0 - self.dirty = 1 + self.dirty = True self.accept("render-texture-targets-changed", self.refreshReadout) if ConfigVariableBool("show-buffers", 0): - self.enable(1) + self.enable(True) - def refreshReadout(self): + def refreshReadout(self) -> None: """Force the readout to be refreshed. This is usually invoked by GraphicsOutput::add_render_texture (via an event handler). However, it is also possible to invoke it manually. Currently, the only time I know of that this is necessary is after a window resize (and I ought to fix that).""" - self.dirty = 1 + self.dirty = True # Call enabled again, mainly to ensure that the task has been # started. @@ -95,14 +102,14 @@ class BufferViewer(DirectObject): """Returns true if the buffer viewer is currently enabled.""" return self.enabled - def enable(self, x): + def enable(self, x: bool) -> None: """Turn the buffer viewer on or off. The initial state of the buffer viewer depends on the Config variable 'show-buffers'.""" if x != 0 and x != 1: BufferViewer.notify.error('invalid parameter to BufferViewer.enable') return self.enabled = x - self.dirty = 1 + self.dirty = True if (x and self.task == 0): self.task = taskMgr.add(self.maintainReadout, "buffer-viewer-maintain-readout", priority=1) @@ -218,7 +225,11 @@ class BufferViewer(DirectObject): self.renderParent = renderParent self.dirty = 1 - def analyzeTextureSet(self, x, set): + def analyzeTextureSet( + self, + x: _Texture | GraphicsEngine | list[_Texture | GraphicsEngine], + set: dict[Texture, int], + ) -> None: """Access: private. Converts a list of GraphicsObject, GraphicsEngine, and Texture into a table of Textures.""" @@ -240,7 +251,7 @@ class BufferViewer(DirectObject): else: return - def makeFrame(self, sizex, sizey): + def makeFrame(self, sizex: int, sizey: int) -> NodePath: """Access: private. Each texture card is displayed with a two-pixel wide frame (a ring of black and a ring of white). This routine builds the frame geometry. It is necessary to @@ -287,7 +298,7 @@ class BufferViewer(DirectObject): geomnode.addGeom(geom) return NodePath(geomnode) - def maintainReadout(self, task): + def maintainReadout(self, task: object) -> int: """Access: private. Whenever necessary, rebuilds the entire display from scratch. This is only done when the configuration parameters have changed.""" @@ -295,7 +306,7 @@ class BufferViewer(DirectObject): # If nothing has changed, don't update. if not self.dirty: return Task.cont - self.dirty = 0 + self.dirty = False # Delete the old set of cards. for card in self.cards: @@ -308,8 +319,8 @@ class BufferViewer(DirectObject): return Task.done # Generate the include and exclude sets. - exclude = {} - include = {} + exclude: dict[Texture, int] = {} + include: dict[Texture, int] = {} self.analyzeTextureSet(self.exclude, exclude) self.analyzeTextureSet(self.include, include) @@ -407,6 +418,7 @@ class BufferViewer(DirectObject): bordersize = 4.0 + assert self.win is not None if float(self.sizex) == 0.0 and float(self.sizey) == 0.0: sizey = int(0.4266666667 * self.win.getYSize()) sizex = (sizey * aspectx) // aspecty diff --git a/direct/src/showbase/ExceptionVarDump.py b/direct/src/showbase/ExceptionVarDump.py index aad5850e6f..308c368701 100755 --- a/direct/src/showbase/ExceptionVarDump.py +++ b/direct/src/showbase/ExceptionVarDump.py @@ -35,7 +35,7 @@ def _varDump__init__(self, *args, **kArgs): sReentry = 0 -def _varDump__print(exc): +def _varDump__print(exc) -> None: global sReentry global notify if sReentry > 0: @@ -176,7 +176,7 @@ def _excepthookDumpVars(eType, eValue, tb): oldExcepthook(eType, eValue, origTb) -def install(log, upload): +def install(log: bool, upload: bool) -> None: """Installs the exception hook.""" global oldExcepthook global wantStackDumpLog @@ -192,8 +192,8 @@ def install(log, upload): # thrown by the interpreter don't get created until the # stack has been unwound and an except block has been reached if not hasattr(Exception, '_moved__init__'): - Exception._moved__init__ = Exception.__init__ - Exception.__init__ = _varDump__init__ + Exception._moved__init__ = Exception.__init__ # type: ignore[attr-defined] + Exception.__init__ = _varDump__init__ # type: ignore[method-assign] else: if sys.excepthook is not _excepthookDumpVars: oldExcepthook = sys.excepthook diff --git a/direct/src/showbase/GarbageReport.py b/direct/src/showbase/GarbageReport.py index ca3d02b36a..d04892b1ff 100644 --- a/direct/src/showbase/GarbageReport.py +++ b/direct/src/showbase/GarbageReport.py @@ -1,5 +1,7 @@ """Contains utility classes for debugging memory leaks.""" +from __future__ import annotations + __all__ = ['FakeObject', '_createGarbage', 'GarbageReport', 'GarbageLogger'] from direct.directnotify.DirectNotifyGlobal import directNotify @@ -10,6 +12,7 @@ from direct.showbase.JobManagerGlobal import jobMgr from direct.showbase.MessengerGlobal import messenger from panda3d.core import ConfigVariableBool import gc +from collections.abc import Callable GarbageCycleCountAnnounceEvent = 'announceGarbageCycleDesc2num' @@ -41,9 +44,21 @@ class GarbageReport(Job): If you just want to dump the report to the log, use GarbageLogger.""" notify = directNotify.newCategory("GarbageReport") - def __init__(self, name, log=True, verbose=False, fullReport=False, findCycles=True, - threaded=False, doneCallback=None, autoDestroy=False, priority=None, - safeMode=False, delOnly=False, collect=True): + def __init__( + self, + name: str, + log: bool = True, + verbose: bool = False, + fullReport: bool = False, + findCycles: bool = True, + threaded: bool = False, + doneCallback: Callable[[GarbageReport], object] | None = None, + autoDestroy: bool = False, + priority: int | None = None, + safeMode: bool = False, + delOnly: bool = False, + collect: bool = True + ) -> None: # if autoDestroy is True, GarbageReport will self-destroy after logging # if false, caller is responsible for calling destroy() # if threaded is True, processing will be performed over multiple frames @@ -399,7 +414,7 @@ class GarbageReport(Job): if self._args.autoDestroy: self.destroy() - def destroy(self): + def destroy(self) -> None: #print 'GarbageReport.destroy' del self._args del self.garbage @@ -417,13 +432,13 @@ class GarbageReport(Job): del self._reportStr Job.destroy(self) - def getNumCycles(self): + def getNumCycles(self) -> int: # if the job hasn't run yet, we don't have a numCycles yet return self.numCycles - def getDesc2numDict(self): + def getDesc2numDict(self) -> dict[str, int]: # dict of python-syntax leak -> number of that type of leak - desc2num = {} + desc2num: dict[str, int] = {} for cycleBySyntax in self.cyclesBySyntax: desc2num.setdefault(cycleBySyntax, 0) desc2num[cycleBySyntax] += 1 @@ -563,7 +578,7 @@ class _CFGLGlobals: LastNumCycles = 0 -def checkForGarbageLeaks(): +def checkForGarbageLeaks() -> int: gc.collect() numGarbage = len(gc.garbage) if numGarbage > 0 and ConfigVariableBool('auto-garbage-logging', False): @@ -576,6 +591,7 @@ def checkForGarbageLeaks(): messenger.send(GarbageCycleCountAnnounceEvent, [gr.getDesc2numDict()]) gr.destroy() notify = directNotify.newCategory("GarbageDetect") + func: Callable[[str], object] if ConfigVariableBool('allow-garbage-cycles', True): func = notify.warning else: @@ -584,7 +600,8 @@ def checkForGarbageLeaks(): return numGarbage -def b_checkForGarbageLeaks(wantReply=False): +def b_checkForGarbageLeaks(wantReply: bool = False) -> int: + from direct.showbase.ShowBaseGlobal import base, __dev__ if not __dev__: return 0 # does a garbage collect on the client and the AI @@ -592,10 +609,10 @@ def b_checkForGarbageLeaks(wantReply=False): # logs leak info and terminates (if configured to do so) try: # if this is the client, tell the AI to check for leaks too - base.cr.timeManager + base.cr.timeManager # type: ignore[attr-defined] except Exception: pass else: - if base.cr.timeManager: - base.cr.timeManager.d_checkForGarbageLeaks(wantReply=wantReply) + if base.cr.timeManager: # type: ignore[attr-defined] + base.cr.timeManager.d_checkForGarbageLeaks(wantReply=wantReply) # type: ignore[attr-defined] return checkForGarbageLeaks() diff --git a/direct/src/showbase/Messenger.py b/direct/src/showbase/Messenger.py index cac8fbd754..c6957819ea 100644 --- a/direct/src/showbase/Messenger.py +++ b/direct/src/showbase/Messenger.py @@ -596,7 +596,7 @@ class Messenger: break return matches - def __methodRepr(self, method): + def __methodRepr(self, method: object) -> str: """ return string version of class.method or method. """ diff --git a/direct/src/showbase/OnScreenDebug.py b/direct/src/showbase/OnScreenDebug.py index 0d690e3806..16292fa628 100755 --- a/direct/src/showbase/OnScreenDebug.py +++ b/direct/src/showbase/OnScreenDebug.py @@ -1,7 +1,11 @@ """Contains the OnScreenDebug class.""" +from __future__ import annotations + __all__ = ['OnScreenDebug'] +from typing import Any + from panda3d.core import ( ConfigVariableBool, ConfigVariableDouble, @@ -18,13 +22,13 @@ class OnScreenDebug: enabled = ConfigVariableBool("on-screen-debug-enabled", False) - def __init__(self): - self.onScreenText = None + def __init__(self) -> None: + self.onScreenText: OnscreenText.OnscreenText | None = None self.frame = 0 self.text = "" - self.data = {} + self.data: dict[str, tuple[int, Any]] = {} - def load(self): + def load(self) -> None: if self.onScreenText: return @@ -40,6 +44,7 @@ class OnScreenDebug: fgColor.setW(ConfigVariableDouble("on-screen-debug-fg-alpha", 0.85).value) bgColor.setW(ConfigVariableDouble("on-screen-debug-bg-alpha", 0.85).value) + from direct.showbase.ShowBaseGlobal import base font = base.loader.loadFont(fontPath) if not font.isValid(): print("failed to load OnScreenDebug font %s" % fontPath) @@ -47,15 +52,16 @@ class OnScreenDebug: self.onScreenText = OnscreenText.OnscreenText( parent = base.a2dTopLeft, pos = (0.0, -0.1), fg=fgColor, bg=bgColor, scale = (fontScale, fontScale, 0.0), - align = TextNode.ALeft, mayChange = 1, font = font) + align = TextNode.ALeft, mayChange = True, font = font) # Make sure readout is never lit or drawn in wireframe DirectUtil.useDirectRenderStyle(self.onScreenText) - def render(self): + def render(self) -> None: if not self.enabled: return if not self.onScreenText: self.load() + assert self.onScreenText is not None self.onScreenText.clearText() for k, v in sorted(self.data.items()): if v[0] == self.frame: @@ -75,7 +81,7 @@ class OnScreenDebug: self.onScreenText.appendText(self.text) self.frame += 1 - def clear(self): + def clear(self) -> None: self.text = "" if self.onScreenText: self.onScreenText.clearText() diff --git a/direct/src/showbase/PythonUtil.py b/direct/src/showbase/PythonUtil.py index 60a19a7700..2d37df1284 100644 --- a/direct/src/showbase/PythonUtil.py +++ b/direct/src/showbase/PythonUtil.py @@ -233,7 +233,7 @@ if __debug__: #----------------------------------------------------------------------------- - def traceFunctionCall(frame): + def traceFunctionCall(frame: types.FrameType) -> str: """ return a string that shows the call frame with calling arguments. e.g. @@ -274,7 +274,7 @@ if __debug__: r+="*** undefined ***" return r+')' - def traceParentCall(): + def traceParentCall() -> str: return traceFunctionCall(sys._getframe(2)) def printThisCall(): @@ -1002,7 +1002,7 @@ def lineupPos(i, num, spacing): return pos - ((float(spacing) * (num-1))/2.) -def formatElapsedSeconds(seconds): +def formatElapsedSeconds(seconds: float) -> str: """ Returns a string of the form "mm:ss" or "hh:mm:ss" or "n days", representing the indicated elapsed time in seconds. @@ -1408,14 +1408,16 @@ def _getSafeReprNotify(): return safeReprNotify -def safeRepr(obj): +def safeRepr(obj: object) -> str: global dtoolSuperBase if dtoolSuperBase is None: _getDtoolSuperBase() + assert dtoolSuperBase is not None global safeReprNotify if safeReprNotify is None: _getSafeReprNotify() + assert safeReprNotify is not None if isinstance(obj, dtoolSuperBase): # repr of C++ object could crash, particularly if the object has been deleted @@ -1447,7 +1449,12 @@ def safeReprTypeOnFail(obj): return '<** FAILED REPR OF %s instance at %s **>' % (obj.__class__.__name__, hex(id(obj))) -def fastRepr(obj, maxLen=200, strFactor=10, _visitedIds=None): +def fastRepr( + obj: object, + maxLen: int = 200, + strFactor: int = 10, + _visitedIds: set[int] | None = None, +) -> str: """ caps the length of iterable types, so very large objects will print faster. also prevents infinite recursion """ try: @@ -1456,9 +1463,9 @@ def fastRepr(obj, maxLen=200, strFactor=10, _visitedIds=None): if id(obj) in _visitedIds: return '' % itype(obj) if type(obj) in (tuple, list): + assert isinstance(obj, (tuple, list)) s = '' - s += {tuple: '(', - list: '[',}[type(obj)] + s += '(' if type(obj) == tuple else '[' if maxLen is not None and len(obj) > maxLen: o = obj[:maxLen] ellips = '...' @@ -1471,8 +1478,7 @@ def fastRepr(obj, maxLen=200, strFactor=10, _visitedIds=None): s += ', ' _visitedIds.remove(id(obj)) s += ellips - s += {tuple: ')', - list: ']',}[type(obj)] + s += ')' if type(obj) == tuple else ']' return s elif type(obj) is dict: s = '{' @@ -1624,7 +1630,7 @@ class Sync: self._name, self._series, self._value) -def itype(obj): +def itype(obj: object) -> type | str: # version of type that gives more complete information about instance types global dtoolSuperBase t = type(obj) @@ -1632,6 +1638,7 @@ def itype(obj): # check if this is a C++ object if dtoolSuperBase is None: _getDtoolSuperBase() + assert dtoolSuperBase is not None if isinstance(obj, dtoolSuperBase): return "" % (obj.__class__) return t diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index ce0de27579..b80931c597 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -177,6 +177,8 @@ class ShowBase(DirectObject.DirectObject): aspect2d: NodePath pixel2d: NodePath + a2dTopLeft: NodePath + cluster: Any | None def __init__(self, fStartDirect: bool = True, windowType: str | None = None) -> None: @@ -438,7 +440,7 @@ class ShowBase(DirectObject.DirectObject): self.useTrackball() #: `.Loader.Loader` object. - self.loader = ShowBaseGlobal.loader + self.loader: Loader.Loader = ShowBaseGlobal.loader self.loader._init_base(self) self.graphicsEngine.setDefaultLoader(self.loader.loader) @@ -654,7 +656,7 @@ class ShowBase(DirectObject.DirectObject): def getExitErrorCode(self): return 0 - def printEnvDebugInfo(self): + def printEnvDebugInfo(self) -> None: """Print some information about the environment that we are running in. Stuff like the model paths and other paths. Feel free to add stuff to this. From 00b5357b8ebfe521851f296b7d555d3ba1cf16f6 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Aug 2025 12:22:47 +0200 Subject: [PATCH 106/291] task: More robust handling of iterators without send()/throw() --- panda/src/event/pythonTask.cxx | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index a978b9d613..2e2213eacc 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -638,17 +638,31 @@ do_python_task() { // We are calling a generator. Use "send" rather than PyIter_Next since // we need to be able to read the value from a StopIteration exception. PyObject *func = PyObject_GetAttrString(_generator, "send"); - nassertr(func != nullptr, DS_interrupt); - result = PyObject_CallOneArg(func, Py_None); - Py_DECREF(func); + if (func != nullptr) { + result = PyObject_CallOneArg(func, Py_None); + Py_DECREF(func); + } else { + // It has no send(), just call next() directly. + nassertr(Py_TYPE(_generator)->tp_iternext != nullptr, DS_interrupt); + PyErr_Clear(); + result = Py_TYPE(_generator)->tp_iternext(_generator); + } } else { // Throw a CancelledError into the generator. _must_cancel = false; - PyObject *exc = PyObject_CallNoArgs(Extension::get_cancelled_error_type()); + PyObject *exc_type = Extension::get_cancelled_error_type(); PyObject *func = PyObject_GetAttrString(_generator, "throw"); - result = PyObject_CallFunctionObjArgs(func, exc, nullptr); - Py_DECREF(func); - Py_DECREF(exc); + if (func != nullptr) { + PyObject *exc = PyObject_CallNoArgs(exc_type); + result = PyObject_CallFunctionObjArgs(func, exc, nullptr); + Py_DECREF(exc); + Py_DECREF(func); + } else { + // If it has no throw(), assume it propagated the CancelledException. + PyErr_Clear(); + PyErr_SetNone(exc_type); + result = nullptr; + } } if (result == nullptr) { From d63f708e2c2340d7c551f2ed2697b8b7fe0cabe8 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Aug 2025 12:59:18 +0200 Subject: [PATCH 107/291] pstats: Actually support large frames (remove check) Fixes #1574 --- panda/src/pstatclient/pStatFrameData.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/panda/src/pstatclient/pStatFrameData.cxx b/panda/src/pstatclient/pStatFrameData.cxx index aef68b3d9e..4397a3a5de 100644 --- a/panda/src/pstatclient/pStatFrameData.cxx +++ b/panda/src/pstatclient/pStatFrameData.cxx @@ -34,7 +34,7 @@ sort_time() { */ bool PStatFrameData:: write_datagram(Datagram &destination, PStatClient *client) const { - if (_time_data.size() >= 65536 || _level_data.size() >= 65536) { + if (_time_data.size() >= INT_MAX || _level_data.size() >= INT_MAX) { pstats_cat.info() << "Dropping frame with " << _time_data.size() << " time measurements and " << _level_data.size() @@ -63,7 +63,7 @@ write_datagram(Datagram &destination, PStatClient *client) const { ptr += 2; } - *(uint32_t *)ptr = __builtin_bswap16(_level_data.size()); + *(uint32_t *)ptr = __builtin_bswap32(_level_data.size()); ptr += 2; for (const DataPoint &dp : _level_data) { From d1f4be7305ddcae6b2dbb484e288c3eed71de106 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Aug 2025 14:19:27 +0200 Subject: [PATCH 108/291] py_compat: Add PyUnstable_Object_IsUniquelyReferenced --- dtool/src/interrogatedb/py_compat.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dtool/src/interrogatedb/py_compat.h b/dtool/src/interrogatedb/py_compat.h index 70c8b43e91..efbdc74d83 100644 --- a/dtool/src/interrogatedb/py_compat.h +++ b/dtool/src/interrogatedb/py_compat.h @@ -368,6 +368,12 @@ PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result) { # define Py_END_CRITICAL_SECTION2() } #endif +/* Python 3.14 */ + +#if PY_VERSION_HEX < 0x030E00A8 +# define PyUnstable_Object_IsUniquelyReferenced(op) (Py_REFCNT((op)) == 1) +#endif + /* Other Python implementations */ #endif // HAVE_PYTHON From cc6674643ea6ec9c199fedd7651ccb23f09225ab Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Aug 2025 14:20:04 +0200 Subject: [PATCH 109/291] tests: Fix test_typed_writable_subclass for Python 3.14 [skip ci] --- tests/putil/test_custom_writable.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/putil/test_custom_writable.py b/tests/putil/test_custom_writable.py index 202c12320c..1ac5499d3f 100644 --- a/tests/putil/test_custom_writable.py +++ b/tests/putil/test_custom_writable.py @@ -32,6 +32,7 @@ BamReader.register_factory(CustomObject.get_class_type(), CustomObject.make_from def test_typed_writable_subclass(): obj = CustomObject() obj.field = 123 + base_rc = sys.getrefcount(obj) assert obj.get_type() == CustomObject.get_class_type() assert obj.type == CustomObject.get_class_type() @@ -45,9 +46,9 @@ def test_typed_writable_subclass(): reader = BamReader(buf) reader.init() obj = reader.read_object() - assert sys.getrefcount(obj) == 3 + assert sys.getrefcount(obj) == base_rc + 1 reader.resolve() del reader - assert sys.getrefcount(obj) == 2 + assert sys.getrefcount(obj) == base_rc assert obj.field == 123 From aa554a2130a2b22fef1acd70956a3f12e31d992d Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 28 Aug 2025 10:24:34 +0200 Subject: [PATCH 110/291] task: Backport fix for generators without send() Backport of 00b5357b8ebfe521851f296b7d555d3ba1cf16f6 --- panda/src/event/pythonTask.cxx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index 8395bd5adc..4b3a79f7ef 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -534,9 +534,15 @@ do_python_task() { // We are calling a generator. Use "send" rather than PyIter_Next since // we need to be able to read the value from a StopIteration exception. PyObject *func = PyObject_GetAttrString(_generator, "send"); - nassertr(func != nullptr, DS_interrupt); - result = PyObject_CallFunctionObjArgs(func, Py_None, nullptr); - Py_DECREF(func); + if (func != nullptr) { + result = PyObject_CallOneArg(func, Py_None); + Py_DECREF(func); + } else { + // It has no send(), just call next() directly. + nassertr(Py_TYPE(_generator)->tp_iternext != nullptr, DS_interrupt); + PyErr_Clear(); + result = Py_TYPE(_generator)->tp_iternext(_generator); + } if (result == nullptr) { // An error happened. If StopIteration, that indicates the task has From 86ba156a7bba32275785a466553bf324e5271b5a Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 28 Aug 2025 11:33:12 +0200 Subject: [PATCH 111/291] py_compat: Update for Python 3.13 and 3.14 --- dtool/src/interrogatedb/py_compat.h | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/dtool/src/interrogatedb/py_compat.h b/dtool/src/interrogatedb/py_compat.h index a1ec1958ab..4b58a9e3b8 100644 --- a/dtool/src/interrogatedb/py_compat.h +++ b/dtool/src/interrogatedb/py_compat.h @@ -287,6 +287,77 @@ INLINE bool PyLong_IsNonNegative(PyObject *value) { # define PyLong_AsInt(x) (_PyLong_AsInt(x)) #endif +#if PY_VERSION_HEX < 0x030D00A1 +ALWAYS_INLINE int +PyModule_Add(PyObject *mod, const char *name, PyObject *value) { + int res = PyModule_AddObjectRef(mod, name, value); + Py_XDECREF(value); + return res; +} +#endif + +#if PY_VERSION_HEX < 0x030D00A1 +INLINE int +PyDict_GetItemRef(PyObject *mp, PyObject *key, PyObject **result) { +#if PY_MAJOR_VERSION >= 3 + PyObject *item = PyDict_GetItemWithError(mp, key); +#else + PyObject *item = _PyDict_GetItemWithError(mp, key); +#endif + if (item != nullptr) { + *result = Py_NewRef(item); + return 1; + } + *result = nullptr; + return PyErr_Occurred() ? -1 : 0; +} + +INLINE int +PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result) { + PyObject *item = nullptr; +#if PY_MAJOR_VERSION >= 3 + PyObject *key_obj = PyUnicode_FromString(key); + item = key_obj ? PyDict_GetItemWithError(mp, key_obj) : nullptr; +#else + PyObject *key_obj = PyString_FromString(key); + item = key_obj ? _PyDict_GetItemWithError(mp, key_obj) : nullptr; +#endif + Py_DECREF(key_obj); + if (item != nullptr) { + *result = Py_NewRef(item); + return 1; + } + *result = nullptr; + return PyErr_Occurred() ? -1 : 0; +} +#endif + +#if PY_VERSION_HEX >= 0x03050200 && PY_VERSION_HEX < 0x030D00A1 +# define PyThreadState_GetUnchecked() (_PyThreadState_UncheckedGet()) +#endif + +#if PY_VERSION_HEX < 0x030D00A2 +# define PyList_Extend(list, iterable) (PyList_SetSlice((list), PY_SSIZE_T_MAX, PY_SSIZE_T_MAX, (iterable))) +# define PyList_Clear(list) (PyList_SetSlice((list), 0, PY_SSIZE_T_MAX, nullptr)) +#endif + +#if PY_VERSION_HEX < 0x030D00A4 +# define PyList_GetItemRef(op, index) (Py_XNewRef(PyList_GetItem((op), (index)))) +#endif + +#if PY_VERSION_HEX < 0x030D00B3 +# define Py_BEGIN_CRITICAL_SECTION(op) { +# define Py_END_CRITICAL_SECTION() } +# define Py_BEGIN_CRITICAL_SECTION2(a, b) { +# define Py_END_CRITICAL_SECTION2() } +#endif + +/* Python 3.14 */ + +#if PY_VERSION_HEX < 0x030E00A8 +# define PyUnstable_Object_IsUniquelyReferenced(op) (Py_REFCNT((op)) == 1) +#endif + /* Other Python implementations */ // _PyErr_OCCURRED is an undocumented macro version of PyErr_Occurred. From b417d6a3738423440afa4dfaa897dbb927cbf753 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 28 Aug 2025 11:34:37 +0200 Subject: [PATCH 112/291] backport: A few thread safety things for free-threaded builds --- panda/src/event/pythonTask.cxx | 12 ++++++++++++ panda/src/pgraph/nodePath_ext.cxx | 8 ++++++++ panda/src/pgraph/pandaNode_ext.cxx | 8 ++++++++ panda/src/pstatclient/pStatClient_ext.cxx | 19 +++++++++++++++++-- 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index 4b3a79f7ef..f5af3f48de 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -368,6 +368,17 @@ __getattr__(PyObject *attr) const { // tp_getattro slot (a la __getattribute__). So, we won't get here when the // attribute has already been found via other methods. +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + PyObject *item = nullptr; + if (PyDict_GetItemRef(__dict__, attr, &item) == 0) { + // PyDict_GetItemRef does not raise an exception on missing attribute. + PyErr_Format(PyExc_AttributeError, + "'PythonTask' object has no attribute '%U'", + attr); + } + return item; + +#else // 3.12 and lower PyObject *item = PyDict_GetItem(__dict__, attr); if (item == nullptr) { @@ -387,6 +398,7 @@ __getattr__(PyObject *attr) const { // PyDict_GetItem returns a borrowed reference. Py_INCREF(item); return item; +#endif } /** diff --git a/panda/src/pgraph/nodePath_ext.cxx b/panda/src/pgraph/nodePath_ext.cxx index c4f7925f24..61e6e74907 100644 --- a/panda/src/pgraph/nodePath_ext.cxx +++ b/panda/src/pgraph/nodePath_ext.cxx @@ -55,6 +55,13 @@ PyObject *Extension:: __deepcopy__(PyObject *self, PyObject *memo) const { extern struct Dtool_PyTypedObject Dtool_NodePath; +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + PyObject *dupe; + if (PyDict_GetItemRef(memo, self, &dupe) != 0) { + // Already in the memo dictionary (or an error happened). + return dupe; + } +#else // Borrowed reference. PyObject *dupe = PyDict_GetItem(memo, self); if (dupe != nullptr) { @@ -62,6 +69,7 @@ __deepcopy__(PyObject *self, PyObject *memo) const { Py_INCREF(dupe); return dupe; } +#endif NodePath *np_dupe; if (_this->is_empty()) { diff --git a/panda/src/pgraph/pandaNode_ext.cxx b/panda/src/pgraph/pandaNode_ext.cxx index d4071ff4c6..95e04aca14 100644 --- a/panda/src/pgraph/pandaNode_ext.cxx +++ b/panda/src/pgraph/pandaNode_ext.cxx @@ -46,6 +46,13 @@ PyObject *Extension:: __deepcopy__(PyObject *self, PyObject *memo) const { extern struct Dtool_PyTypedObject Dtool_PandaNode; +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + PyObject *dupe; + if (PyDict_GetItemRef(memo, self, &dupe) != 0) { + // Already in the memo dictionary (or an error happened). + return dupe; + } +#else // Borrowed reference. PyObject *dupe = PyDict_GetItem(memo, self); if (dupe != nullptr) { @@ -53,6 +60,7 @@ __deepcopy__(PyObject *self, PyObject *memo) const { Py_INCREF(dupe); return dupe; } +#endif PT(PandaNode) node_dupe = _this->copy_subgraph(); diff --git a/panda/src/pstatclient/pStatClient_ext.cxx b/panda/src/pstatclient/pStatClient_ext.cxx index 684ac1db2a..6071b7dfbc 100644 --- a/panda/src/pstatclient/pStatClient_ext.cxx +++ b/panda/src/pstatclient/pStatClient_ext.cxx @@ -73,11 +73,21 @@ __declspec(noinline) make_python_frame_collector(PyFrameObject *frame, PyCodeObject *code) { #if PY_VERSION_HEX >= 0x030B0000 // 3.11 // Fetch the module name out of the frame's global scope. + const char *mod_name = ""; + PyObject *py_mod_name = nullptr; PyObject *globals = PyFrame_GetGlobals(frame); - PyObject *py_mod_name = PyDict_GetItemString(globals, "__name__"); +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + if (PyDict_GetItemStringRef(globals, "__name__", &py_mod_name) > 0) { + mod_name = PyUnicode_AsUTF8(py_mod_name); + } +#else + py_mod_name = PyDict_GetItemString(globals, "__name__"); + if (py_mod_name != nullptr) { + mod_name = PyUnicode_AsUTF8(py_mod_name); + } +#endif Py_DECREF(globals); - const char *mod_name = py_mod_name ? PyUnicode_AsUTF8(py_mod_name) : ""; const char *meth_name = PyUnicode_AsUTF8(code->co_qualname); char buffer[1024]; size_t len = snprintf(buffer, sizeof(buffer), "%s:%s", mod_name, meth_name); @@ -86,6 +96,11 @@ make_python_frame_collector(PyFrameObject *frame, PyCodeObject *code) { buffer[i] = ':'; } } + +#if PY_VERSION_HEX >= 0x030D00A1 // 3.13 + Py_XDECREF(py_mod_name); +#endif + #else // Try to figure out the type name. There's no obvious way to do this. // It's possible that the first argument passed to this function is the From 2625c19ccf81c7dea756c3b07026fb9f87c6a166 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 28 Aug 2025 12:31:34 +0200 Subject: [PATCH 113/291] build: Update to interrogate 0.6.1 --- dtool/Config.cmake | 2 +- makepanda/makepandacore.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dtool/Config.cmake b/dtool/Config.cmake index fbe2d24836..912f9d4bbb 100644 --- a/dtool/Config.cmake +++ b/dtool/Config.cmake @@ -265,7 +265,7 @@ if(BUILD_INTERROGATE) panda3d-interrogate GIT_REPOSITORY https://github.com/panda3d/interrogate.git - GIT_TAG 502b4806427f456e5fdbf4f98f271f89622a8a9c + GIT_TAG 4a9b7d2d9e40edf6bfb3177521b5481130291bda PREFIX ${_interrogate_dir} CMAKE_ARGS diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index cddf8f5e4a..71a7df38fd 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -589,8 +589,8 @@ def GetInterrogateDir(): return INTERROGATE_DIR dir = os.path.join(GetOutputDir(), "tmp", "interrogate") - if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.6.0.dist-info")): - oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.6.0" % (sys.executable, dir)) + if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.6.1.dist-info")): + oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.6.1" % (sys.executable, dir)) INTERROGATE_DIR = dir From 469df73ce741e7559abb96ce2f897a6999370698 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 28 Aug 2025 12:31:51 +0200 Subject: [PATCH 114/291] tests: Add unit tests for send() and throw() on generator wrapper --- tests/event/test_futures.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/event/test_futures.py b/tests/event/test_futures.py index ee6b0f2894..4aa11715e4 100644 --- a/tests/event/test_futures.py +++ b/tests/event/test_futures.py @@ -46,6 +46,31 @@ def check_result(fut, expected): return True +def test_future_await_send(): + fut = core.AsyncFuture() + + i = fut.__await__() + assert i.send(None) == fut + assert i.send(None) == fut + assert i.send(None) == fut + assert i.send(None) == fut + + fut.set_result(123) + + with pytest.raises(StopIteration) as e: + i.send(None) + + assert e.value.value == 123 + + +def test_future_await_throw(): + fut = core.AsyncFuture() + + i = fut.__await__() + with pytest.raises(RuntimeError): + i.throw(RuntimeError) + + def test_future_cancelled(): fut = core.AsyncFuture() From f5713d1240f327a8dd84c724617365419956fbdd Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 3 Sep 2025 23:36:04 +0200 Subject: [PATCH 115/291] cleanup: Fix missing `std::` prefixes --- direct/src/dcparser/dcClass_ext.cxx | 2 +- dtool/src/dtoolutil/iostream_ext.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/direct/src/dcparser/dcClass_ext.cxx b/direct/src/dcparser/dcClass_ext.cxx index 65f55bf33b..c6b6b4465c 100644 --- a/direct/src/dcparser/dcClass_ext.cxx +++ b/direct/src/dcparser/dcClass_ext.cxx @@ -108,7 +108,7 @@ receive_update(PyObject *distobj, DatagramIterator &di) const { int field_id = packer.raw_unpack_uint16(); DCField *field = _this->get_field_by_index(field_id); if (field == nullptr) { - ostringstream strm; + std::ostringstream strm; strm << "Received update for field " << field_id << ", not in class " << _this->get_name(); diff --git a/dtool/src/dtoolutil/iostream_ext.h b/dtool/src/dtoolutil/iostream_ext.h index 54d3ab1a43..a3963d158d 100644 --- a/dtool/src/dtoolutil/iostream_ext.h +++ b/dtool/src/dtoolutil/iostream_ext.h @@ -22,6 +22,9 @@ #include #include "py_panda.h" +using std::ostream; +using std::istream; + /** * These classes define the extension methods for istream and ostream, which * are called instead of any C++ methods with the same prototype. From 2ab3ea39c4037eaee973c2c69b032d612e0105fe Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 3 Sep 2025 23:36:43 +0200 Subject: [PATCH 116/291] interrogatedb: Name interrogate_request structs This allows them to be forward-declared --- dtool/src/interrogatedb/interrogate_request.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dtool/src/interrogatedb/interrogate_request.h b/dtool/src/interrogatedb/interrogate_request.h index 46a80b27b6..fdc2f093b8 100644 --- a/dtool/src/interrogatedb/interrogate_request.h +++ b/dtool/src/interrogatedb/interrogate_request.h @@ -44,12 +44,12 @@ EXPCL_INTERROGATEDB void interrogate_request_database(const char *database_filen /* The more sophisticated interface uses these structures. */ -typedef struct { +typedef struct InterrogateUniqueNameDef { const char *name; int index_offset; } InterrogateUniqueNameDef; -typedef struct { +typedef struct InterrogateModuleDef { int file_identifier; const char *library_name; From 0c0eca60d35e33a207737625c3c16f411507fc5f Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 3 Sep 2025 23:38:05 +0200 Subject: [PATCH 117/291] express: Error check in set_encryption_password, don't use Dtool_Return_None --- panda/src/express/multifile_ext.I | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/panda/src/express/multifile_ext.I b/panda/src/express/multifile_ext.I index 68e427af2e..0774a3745b 100644 --- a/panda/src/express/multifile_ext.I +++ b/panda/src/express/multifile_ext.I @@ -24,8 +24,11 @@ set_encryption_password(PyObject *encryption_password) const { // Have we been passed a string? if (PyUnicode_Check(encryption_password)) { const char *pass_str = PyUnicode_AsUTF8AndSize(encryption_password, &pass_len); + if (pass_str == NULL) { + return NULL; + } _this->set_encryption_password(std::string(pass_str, pass_len)); - return Dtool_Return_None(); + Py_RETURN_NONE; } // Have we been passed a bytes object? @@ -46,7 +49,7 @@ set_encryption_password(PyObject *encryption_password) const { } _this->set_encryption_password(std::string(pass_str, pass_len)); - return Dtool_Return_None(); + Py_RETURN_NONE; } return Dtool_Raise_BadArgumentsError( From 05a610fadf7591a932e085f0300317df89b98789 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 4 Sep 2025 00:44:12 +0200 Subject: [PATCH 118/291] build: Update to interrogate 0.7.0 --- dtool/Config.cmake | 2 +- dtool/src/interrogatedb/CMakeLists.txt | 1 - dtool/src/interrogatedb/py_panda.I | 77 +------- dtool/src/interrogatedb/py_panda.h | 242 +------------------------ dtool/src/interrogatedb/py_wrappers.h | 61 ------- makepanda/makepandacore.py | 4 +- 6 files changed, 13 insertions(+), 374 deletions(-) delete mode 100644 dtool/src/interrogatedb/py_wrappers.h diff --git a/dtool/Config.cmake b/dtool/Config.cmake index 912f9d4bbb..dff731ccd1 100644 --- a/dtool/Config.cmake +++ b/dtool/Config.cmake @@ -265,7 +265,7 @@ if(BUILD_INTERROGATE) panda3d-interrogate GIT_REPOSITORY https://github.com/panda3d/interrogate.git - GIT_TAG 4a9b7d2d9e40edf6bfb3177521b5481130291bda + GIT_TAG c19b93c00bd31ecdde861f6511e7830d9f69ac91 PREFIX ${_interrogate_dir} CMAKE_ARGS diff --git a/dtool/src/interrogatedb/CMakeLists.txt b/dtool/src/interrogatedb/CMakeLists.txt index f137a7b802..0457184feb 100644 --- a/dtool/src/interrogatedb/CMakeLists.txt +++ b/dtool/src/interrogatedb/CMakeLists.txt @@ -2,7 +2,6 @@ set(P3IGATERUNTIME_HEADERS interrogate_request.h py_compat.h py_panda.h py_panda.I - py_wrappers.h ) install(FILES ${P3IGATERUNTIME_HEADERS} COMPONENT CoreDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d) diff --git a/dtool/src/interrogatedb/py_panda.I b/dtool/src/interrogatedb/py_panda.I index f3cb5a1d12..edfd0690f8 100644 --- a/dtool/src/interrogatedb/py_panda.I +++ b/dtool/src/interrogatedb/py_panda.I @@ -59,63 +59,6 @@ DtoolInstance_GetPointer(PyObject *self, T *&into, Dtool_PyTypedObject &target_c return false; } -/** - * Function to create a hash from a wrapped Python object. - */ -INLINE Py_hash_t DtoolInstance_HashPointer(PyObject *self) { - if (self != nullptr && DtoolInstance_Check(self)) { - return (Py_hash_t)(intptr_t)DtoolInstance_VOID_PTR(self); - } - return -1; -} - -/** - * Python 2-style comparison function that compares objects by pointer. - */ -INLINE int DtoolInstance_ComparePointers(PyObject *v1, PyObject *v2) { - void *v1_this = DtoolInstance_Check(v1) ? DtoolInstance_VOID_PTR(v1) : nullptr; - void *v2_this = DtoolInstance_Check(v2) ? DtoolInstance_VOID_PTR(v2) : nullptr; - if (v1_this != nullptr && v2_this != nullptr) { - return (v1_this > v2_this) - (v1_this < v2_this); - } else { - return (v1 > v2) - (v1 < v2); - } -} - -/** - * Rich comparison function that compares objects by pointer. - */ -INLINE PyObject *DtoolInstance_RichComparePointers(PyObject *v1, PyObject *v2, int op) { - int cmpval = DtoolInstance_ComparePointers(v1, v2); - Py_RETURN_RICHCOMPARE(cmpval, 0, op); -} - -/** - * Utility function for assigning a PyObject pointer while managing refcounts. - */ -ALWAYS_INLINE void -Dtool_Assign_PyObject(PyObject *&ptr, PyObject *value) { - PyObject *prev_value = ptr; - if (prev_value != value) { - ptr = Py_XNewRef(value); - Py_XDECREF(prev_value); - } -} - -/** - * Converts the enum value to a C long. - */ -INLINE long Dtool_EnumValue_AsLong(PyObject *value) { - PyObject *val = PyObject_GetAttrString(value, "value"); - if (val != nullptr) { - long as_long = PyLongOrInt_AS_LONG(val); - Py_DECREF(val); - return as_long; - } else { - return -1; - } -} - /** * These functions wrap a pointer for a class that defines get_type_handle(). */ @@ -167,23 +110,6 @@ DTool_PyInit_Finalize(PyObject *self, void *local_this, Dtool_PyTypedObject *typ return 0; } -/** - * Checks that the tuple is empty. - */ -ALWAYS_INLINE bool -Dtool_CheckNoArgs(PyObject *args) { - return PyTuple_GET_SIZE(args) == 0; -} - -/** - * Checks that the tuple is empty, and that the dict is empty or NULL. - */ -ALWAYS_INLINE bool -Dtool_CheckNoArgs(PyObject *args, PyObject *kwds) { - return PyTuple_GET_SIZE(args) == 0 && - (kwds == nullptr || PyDict_GET_SIZE(kwds) == 0); -} - /** * The following functions wrap an arbitrary C++ value into a PyObject. */ @@ -324,7 +250,8 @@ ALWAYS_INLINE PyObject *Dtool_WrapValue(PyObject *value) { return value; } -ALWAYS_INLINE PyObject *Dtool_WrapValue(const vector_uchar &value) { +template +ALWAYS_INLINE PyObject *Dtool_WrapValue(const std::vector &value) { #if PY_MAJOR_VERSION >= 3 return PyBytes_FromStringAndSize((char *)value.data(), (Py_ssize_t)value.size()); #else diff --git a/dtool/src/interrogatedb/py_panda.h b/dtool/src/interrogatedb/py_panda.h index 4b28908b9a..29a9cd4b28 100644 --- a/dtool/src/interrogatedb/py_panda.h +++ b/dtool/src/interrogatedb/py_panda.h @@ -8,15 +8,14 @@ #include #include #include +#include #ifdef USE_DEBUG_PYTHON #define Py_DEBUG #endif #include "pnotify.h" -#include "vector_uchar.h" #include "register_type.h" -#include "interrogate_request.h" #if defined(HAVE_PYTHON) && !defined(CPPPARSER) @@ -24,8 +23,6 @@ #include "py_compat.h" #include -using namespace std; - // this is tempory .. untill this is glued better into the panda build system #if defined(_WIN32) && !defined(LINK_ALL_STATIC) @@ -86,83 +83,6 @@ struct Dtool_PyTypedObject { CoerceFunction _Dtool_Coerce; }; -// This is now simply a forward declaration. The actual definition is created -// by the code generator. -#define Define_Dtool_Class(MODULE_NAME, CLASS_NAME, PUBLIC_NAME) \ - extern Dtool_PyTypedObject Dtool_##CLASS_NAME; - -// More Macro(s) to Implement class functions.. Usually used if C++ needs type -// information -#define Define_Dtool_new(CLASS_NAME,CNAME)\ -static PyObject *Dtool_new_##CLASS_NAME(PyTypeObject *type, PyObject *args, PyObject *kwds) {\ - (void) args; (void) kwds;\ - PyObject *self = type->tp_alloc(type, 0);\ - ((Dtool_PyInstDef *)self)->_signature = PY_PANDA_SIGNATURE;\ - ((Dtool_PyInstDef *)self)->_My_Type = &Dtool_##CLASS_NAME;\ - return self;\ -} - -// The following used to be in the above macro, but it doesn't seem to be -// necessary as tp_alloc memsets the object to 0. -// ((Dtool_PyInstDef *)self)->_ptr_to_object = NULL; -// ((Dtool_PyInstDef *)self)->_memory_rules = false; -// ((Dtool_PyInstDef *)self)->_is_const = false; - -// Delete functions.. -#ifdef NDEBUG -#define Define_Dtool_FreeInstance_Private(CLASS_NAME,CNAME)\ -static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ - Py_TYPE(self)->tp_free(self);\ -} -#else // NDEBUG -#define Define_Dtool_FreeInstance_Private(CLASS_NAME,CNAME)\ -static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ - if (DtoolInstance_VOID_PTR(self) != nullptr) {\ - if (((Dtool_PyInstDef *)self)->_memory_rules) {\ - std::cerr << "Detected leak for " << #CLASS_NAME \ - << " which interrogate cannot delete.\n"; \ - }\ - }\ - Py_TYPE(self)->tp_free(self);\ -} -#endif // NDEBUG - -#define Define_Dtool_FreeInstance(CLASS_NAME,CNAME)\ -static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ - if (DtoolInstance_VOID_PTR(self) != nullptr) {\ - if (((Dtool_PyInstDef *)self)->_memory_rules) {\ - delete (CNAME *)DtoolInstance_VOID_PTR(self);\ - }\ - }\ - Py_TYPE(self)->tp_free(self);\ -} - -#define Define_Dtool_FreeInstanceRef(CLASS_NAME,CNAME)\ -static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ - if (DtoolInstance_VOID_PTR(self) != nullptr) {\ - if (((Dtool_PyInstDef *)self)->_memory_rules) {\ - unref_delete((CNAME *)DtoolInstance_VOID_PTR(self));\ - }\ - }\ - Py_TYPE(self)->tp_free(self);\ -} - -#define Define_Dtool_FreeInstanceRef_Private(CLASS_NAME,CNAME)\ -static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ - if (DtoolInstance_VOID_PTR(self) != nullptr) {\ - if (((Dtool_PyInstDef *)self)->_memory_rules) {\ - unref_delete((ReferenceCount *)(CNAME *)DtoolInstance_VOID_PTR(self));\ - }\ - }\ - Py_TYPE(self)->tp_free(self);\ -} - -#define Define_Dtool_Simple_FreeInstance(CLASS_NAME, CNAME)\ -static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ - ((Dtool_InstDef_##CLASS_NAME *)self)->_value.~##CLASS_NAME();\ - Py_TYPE(self)->tp_free(self);\ -} - // Extract the PyTypeObject pointer corresponding to a Dtool_PyTypedObject. #define Dtool_GetPyTypeObject(type) (&(type)->_PyType) @@ -178,31 +98,12 @@ static void Dtool_FreeInstance_##CLASS_NAME(PyObject *self) {\ #define DtoolInstance_INIT_PTR(obj, ptr) { ((Dtool_PyInstDef *)obj)->_ptr_to_object = (void*)(ptr); } #define DtoolInstance_UPCAST(obj, type) (((Dtool_PyInstDef *)(obj))->_My_Type->_Dtool_UpcastInterface((obj), &(type))) -// ** HACK ** allert.. Need to keep a runtime type dictionary ... that is -// forward declared of typed object. We rely on the fact that typed objects -// are uniquly defined by an integer. - -#if PY_VERSION_HEX >= 0x030d0000 -class Dtool_TypeMap : public std::map { -public: - PyMutex _lock { 0 }; -}; -#else -typedef std::map Dtool_TypeMap; -#endif - -EXPCL_PYPANDA Dtool_TypeMap *Dtool_GetGlobalTypeMap(); - class DtoolProxy { public: mutable PyObject *_self; TypeHandle _type; }; -EXPCL_PYPANDA void DtoolProxy_Init(DtoolProxy *proxy, PyObject *self, - Dtool_PyTypedObject &classdef, - TypeRegistry::PythonWrapFunc *wrap_func); - /** */ @@ -218,19 +119,6 @@ EXPCL_PYPANDA bool Dtool_Call_ExtractThisPointer_NonConst(PyObject *self, Dtool_ template INLINE bool DtoolInstance_GetPointer(PyObject *self, T *&into); template INLINE bool DtoolInstance_GetPointer(PyObject *self, T *&into, Dtool_PyTypedObject &classdef); -INLINE Py_hash_t DtoolInstance_HashPointer(PyObject *self); -INLINE int DtoolInstance_ComparePointers(PyObject *v1, PyObject *v2); -INLINE PyObject *DtoolInstance_RichComparePointers(PyObject *v1, PyObject *v2, int op); - -// Functions related to error reporting. -EXPCL_PYPANDA bool _Dtool_CheckErrorOccurred(); - -#ifdef NDEBUG -#define Dtool_CheckErrorOccurred() (UNLIKELY(PyErr_Occurred() != nullptr)) -#else -#define Dtool_CheckErrorOccurred() (UNLIKELY(_Dtool_CheckErrorOccurred())) -#endif - EXPCL_PYPANDA PyObject *Dtool_Raise_AssertionError(); EXPCL_PYPANDA PyObject *Dtool_Raise_TypeError(const char *message); EXPCL_PYPANDA PyObject *Dtool_Raise_ArgTypeError(PyObject *obj, int param, const char *function_name, const char *type_name); @@ -251,31 +139,6 @@ EXPCL_PYPANDA int _Dtool_Raise_BadArgumentsError_Int(const char *message); #define Dtool_Raise_BadArgumentsError_Int(x) _Dtool_Raise_BadArgumentsError_Int(x) #endif -// These functions are similar to Dtool_WrapValue, except that they also -// contain code for checking assertions and exceptions when compiling with -// NDEBUG mode on. -EXPCL_PYPANDA PyObject *_Dtool_Return_None(); -EXPCL_PYPANDA PyObject *Dtool_Return_Bool(bool value); -EXPCL_PYPANDA PyObject *_Dtool_Return(PyObject *value); - -#ifdef NDEBUG -#define Dtool_Return_None() (LIKELY(PyErr_Occurred() == nullptr) ? (Py_NewRef(Py_None)) : nullptr) -#define Dtool_Return(value) (LIKELY(PyErr_Occurred() == nullptr) ? value : nullptr) -#else -#define Dtool_Return_None() _Dtool_Return_None() -#define Dtool_Return(value) _Dtool_Return(value) -#endif - -ALWAYS_INLINE void Dtool_Assign_PyObject(PyObject *&ptr, PyObject *value); - -/** - * Wrapper around Python 3.4's enum library, which does not have a C API. - */ -EXPCL_PYPANDA PyTypeObject *Dtool_EnumType_Create(const char *name, PyObject *names, - const char *module = nullptr); -INLINE long Dtool_EnumValue_AsLong(PyObject *value); - - /** */ @@ -292,102 +155,9 @@ template INLINE PyObject *DTool_CreatePyInstance(T *obj, bool memory_ru template INLINE PyObject *DTool_CreatePyInstanceTyped(const T *obj, bool memory_rules); template INLINE PyObject *DTool_CreatePyInstanceTyped(T *obj, bool memory_rules); -// Macro(s) class definition .. Used to allocate storage and init some values -// for a Dtool Py Type object. - -// struct Dtool_PyTypedObject Dtool_##CLASS_NAME; - -#define Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\ -extern struct Dtool_PyTypedObject Dtool_##CLASS_NAME;\ -static int Dtool_Init_##CLASS_NAME(PyObject *self, PyObject *args, PyObject *kwds);\ -static PyObject *Dtool_new_##CLASS_NAME(PyTypeObject *type, PyObject *args, PyObject *kwds); - -#define Define_Module_Class(MODULE_NAME,CLASS_NAME,CNAME,PUBLIC_NAME)\ -Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\ -Define_Dtool_new(CLASS_NAME,CNAME)\ -Define_Dtool_FreeInstance(CLASS_NAME,CNAME)\ -Define_Dtool_Class(MODULE_NAME,CLASS_NAME,PUBLIC_NAME) - -#define Define_Module_Class_Private(MODULE_NAME,CLASS_NAME,CNAME,PUBLIC_NAME)\ -Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\ -Define_Dtool_new(CLASS_NAME,CNAME)\ -Define_Dtool_FreeInstance_Private(CLASS_NAME,CNAME)\ -Define_Dtool_Class(MODULE_NAME,CLASS_NAME,PUBLIC_NAME) - -#define Define_Module_ClassRef_Private(MODULE_NAME,CLASS_NAME,CNAME,PUBLIC_NAME)\ -Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\ -Define_Dtool_new(CLASS_NAME,CNAME)\ -Define_Dtool_FreeInstanceRef_Private(CLASS_NAME,CNAME)\ -Define_Dtool_Class(MODULE_NAME,CLASS_NAME,PUBLIC_NAME) - -#define Define_Module_ClassRef(MODULE_NAME,CLASS_NAME,CNAME,PUBLIC_NAME)\ -Define_Module_Class_Internal(MODULE_NAME,CLASS_NAME,CNAME)\ -Define_Dtool_new(CLASS_NAME,CNAME)\ -Define_Dtool_FreeInstanceRef(CLASS_NAME,CNAME)\ -Define_Dtool_Class(MODULE_NAME,CLASS_NAME,PUBLIC_NAME) - // The finalizer for simple instances. INLINE int DTool_PyInit_Finalize(PyObject *self, void *This, Dtool_PyTypedObject *type, bool memory_rules, bool is_const); -// A heler function to glu methed definition together .. that can not be done -// at code generation time becouse of multiple generation passes in -// interigate.. -typedef std::map MethodDefmap; - -// We need a way to runtime merge compile units into a python "Module" .. this -// is done with the fallowing structors and code.. along with the support of -// interigate_module - -struct Dtool_TypeDef { - const char *const name; - Dtool_PyTypedObject *type; -}; - -struct LibraryDef { - PyMethodDef *const _methods; - const Dtool_TypeDef *const _types; - Dtool_TypeDef *const _external_types; - const InterrogateModuleDef *const _module_def; -}; - -#if PY_MAJOR_VERSION >= 3 -EXPCL_PYPANDA PyObject *Dtool_PyModuleInitHelper(const LibraryDef *defs[], PyModuleDef *module_def); -#else -EXPCL_PYPANDA PyObject *Dtool_PyModuleInitHelper(const LibraryDef *defs[], const char *modulename); -#endif - -// HACK.... Be carefull Dtool_BorrowThisReference This function can be used to -// grab the "THIS" pointer from an object and use it Required to support fom -// historical inharatence in the for of "is this instance of".. -EXPCL_PYPANDA PyObject *Dtool_BorrowThisReference(PyObject *self, PyObject *args); - -#define DTOOL_PyObject_HashPointer DtoolInstance_HashPointer -#define DTOOL_PyObject_ComparePointers DtoolInstance_ComparePointers - -EXPCL_PYPANDA PyObject * -copy_from_make_copy(PyObject *self, PyObject *noargs); - -EXPCL_PYPANDA PyObject * -copy_from_copy_constructor(PyObject *self, PyObject *noargs); - -EXPCL_PYPANDA PyObject * -map_deepcopy_to_copy(PyObject *self, PyObject *args); - -/** - * These functions check whether the arguments passed to a function conform to - * certain expectations. - */ -ALWAYS_INLINE bool Dtool_CheckNoArgs(PyObject *args); -ALWAYS_INLINE bool Dtool_CheckNoArgs(PyObject *args, PyObject *kwds); -EXPCL_PYPANDA bool Dtool_ExtractArg(PyObject **result, PyObject *args, - PyObject *kwds, const char *keyword); -EXPCL_PYPANDA bool Dtool_ExtractArg(PyObject **result, PyObject *args, - PyObject *kwds); -EXPCL_PYPANDA bool Dtool_ExtractOptionalArg(PyObject **result, PyObject *args, - PyObject *kwds, const char *keyword); -EXPCL_PYPANDA bool Dtool_ExtractOptionalArg(PyObject **result, PyObject *args, - PyObject *kwds); - /** * These functions convert a C++ value into the corresponding Python object. * This used to be generated by the code generator, but it seems more reliable @@ -412,7 +182,8 @@ ALWAYS_INLINE PyObject *Dtool_WrapValue(char value); ALWAYS_INLINE PyObject *Dtool_WrapValue(wchar_t value); ALWAYS_INLINE PyObject *Dtool_WrapValue(std::nullptr_t); ALWAYS_INLINE PyObject *Dtool_WrapValue(PyObject *value); -ALWAYS_INLINE PyObject *Dtool_WrapValue(const vector_uchar &value); +template +ALWAYS_INLINE PyObject *Dtool_WrapValue(const std::vector &value); #if PY_MAJOR_VERSION >= 0x02060000 ALWAYS_INLINE PyObject *Dtool_WrapValue(Py_buffer *value); @@ -423,9 +194,12 @@ ALWAYS_INLINE PyObject *Dtool_WrapValue(const std::pair &value); EXPCL_PYPANDA Dtool_PyTypedObject *Dtool_GetSuperBase(); -#include "py_panda.I" +/** + * Creates a Python generator object from a next() function. + */ +EXPCL_PYPANDA PyObject *Dtool_NewGenerator(PyObject *self, iternextfunc func); -#include "py_wrappers.h" +#include "py_panda.I" #endif // HAVE_PYTHON && !CPPPARSER diff --git a/dtool/src/interrogatedb/py_wrappers.h b/dtool/src/interrogatedb/py_wrappers.h deleted file mode 100644 index eb5658cb6b..0000000000 --- a/dtool/src/interrogatedb/py_wrappers.h +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @file py_wrappers.h - * @author rdb - * @date 2017-11-26 - */ - -#ifndef PY_WRAPPERS_H -#define PY_WRAPPERS_H - -#include "py_panda.h" - -#ifdef HAVE_PYTHON - -/** - * These classes are returned from properties that require a subscript - * interface, ie. something.children[i] = 3. - */ -struct Dtool_WrapperBase { - PyObject_HEAD; - PyObject *_self; - const char *_name; -}; - -struct Dtool_SequenceWrapper { - Dtool_WrapperBase _base; - lenfunc _len_func; - ssizeargfunc _getitem_func; -}; - -struct Dtool_MutableSequenceWrapper { - Dtool_WrapperBase _base; - lenfunc _len_func; - ssizeargfunc _getitem_func; - ssizeobjargproc _setitem_func; - PyObject *(*_insert_func)(PyObject *, size_t, PyObject *); -}; - -struct Dtool_MappingWrapper { - union { - Dtool_WrapperBase _base; - Dtool_SequenceWrapper _keys; - }; - binaryfunc _getitem_func; - objobjargproc _setitem_func; -}; - -struct Dtool_GeneratorWrapper { - Dtool_WrapperBase _base; - iternextfunc _iternext_func; -}; - -EXPCL_PYPANDA Dtool_SequenceWrapper *Dtool_NewSequenceWrapper(PyObject *self, const char *name); -EXPCL_PYPANDA Dtool_MutableSequenceWrapper *Dtool_NewMutableSequenceWrapper(PyObject *self, const char *name); -EXPCL_PYPANDA Dtool_MappingWrapper *Dtool_NewMappingWrapper(PyObject *self, const char *name); -EXPCL_PYPANDA Dtool_MappingWrapper *Dtool_NewMutableMappingWrapper(PyObject *self, const char *name); -EXPCL_PYPANDA PyObject *Dtool_NewGenerator(PyObject *self, iternextfunc func); -EXPCL_PYPANDA PyObject *Dtool_NewStaticProperty(PyTypeObject *obj, const PyGetSetDef *getset); - -#endif // HAVE_PYTHON - -#endif // PY_WRAPPERS_H diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 71a7df38fd..1d79c9061c 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -589,8 +589,8 @@ def GetInterrogateDir(): return INTERROGATE_DIR dir = os.path.join(GetOutputDir(), "tmp", "interrogate") - if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.6.1.dist-info")): - oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.6.1" % (sys.executable, dir)) + if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.7.0.dist-info")): + oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.7.0" % (sys.executable, dir)) INTERROGATE_DIR = dir From 32d2a13751156c5fe0f261ce2e5f38a53d0f26a1 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 4 Sep 2025 00:57:37 +0200 Subject: [PATCH 119/291] dtoolutil: Fix missing std:: prefix --- dtool/src/dtoolutil/textEncoder_ext.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtool/src/dtoolutil/textEncoder_ext.cxx b/dtool/src/dtoolutil/textEncoder_ext.cxx index 4774286c5b..21faa26a67 100644 --- a/dtool/src/dtoolutil/textEncoder_ext.cxx +++ b/dtool/src/dtoolutil/textEncoder_ext.cxx @@ -94,7 +94,7 @@ encode_wchar(char32_t ch, TextEncoder::Encoding encoding) { * given encoding. */ PyObject *Extension:: -encode_wtext(const wstring &wtext, TextEncoder::Encoding encoding) { +encode_wtext(const std::wstring &wtext, TextEncoder::Encoding encoding) { std::string value = TextEncoder::encode_wtext(wtext, encoding); return PyBytes_FromStringAndSize((char *)value.data(), (Py_ssize_t)value.size()); } From 212ad7486c2f689457136c36446ffc42c178f559 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 4 Sep 2025 13:31:24 +0200 Subject: [PATCH 120/291] prc: Fix recursion reading notify-timestamp config.prc variable --- dtool/src/prc/configPageManager.cxx | 15 +++++++++++ dtool/src/prc/notifyCategory.cxx | 40 ++++------------------------- dtool/src/prc/notifyCategory.h | 6 +++-- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/dtool/src/prc/configPageManager.cxx b/dtool/src/prc/configPageManager.cxx index 47c206e720..7821617bfd 100644 --- a/dtool/src/prc/configPageManager.cxx +++ b/dtool/src/prc/configPageManager.cxx @@ -492,6 +492,21 @@ reload_implicit_pages() { _currently_loading = false; invalidate_cache(); + // These important variables are updated here to avoid recursion, since + // they may be accessed by the PRC system. + ConfigVariableBool notify_timestamp + ("notify-timestamp", false, + PRC_DESC("Set true to output the date & time with each notify message.")); + NotifyCategory::_notify_timestamp = notify_timestamp; + +#ifndef NDEBUG + ConfigVariableBool check_debug_notify_protect + ("check-debug-notify-protect", false, + PRC_DESC("Set true to issue a warning message if a debug or spam " + "notify output is not protected within an if statement.")); + NotifyCategory::_check_debug_notify_protect = check_debug_notify_protect; +#endif + #ifdef USE_PANDAFILESTREAM // Update this very low-level config variable here, for lack of any better // place. diff --git a/dtool/src/prc/notifyCategory.cxx b/dtool/src/prc/notifyCategory.cxx index 17d2309ab8..3a0b161bf2 100644 --- a/dtool/src/prc/notifyCategory.cxx +++ b/dtool/src/prc/notifyCategory.cxx @@ -21,6 +21,9 @@ #include // for strftime(). #include +bool NotifyCategory::_notify_timestamp = false; +bool NotifyCategory::_check_debug_notify_protect = false; + long NotifyCategory::_server_delta = 0; /** @@ -55,7 +58,7 @@ std::ostream &NotifyCategory:: out(NotifySeverity severity, bool prefix) const { if (is_on(severity)) { if (prefix) { - if (get_notify_timestamp()) { + if (_notify_timestamp) { // Format a timestamp to include as a prefix as well. time_t now = time(nullptr) + _server_delta; struct tm atm; @@ -79,7 +82,7 @@ out(NotifySeverity severity, bool prefix) const { return Notify::out(severity); } - } else if (severity <= NS_debug && get_check_debug_notify_protect()) { + } else if (severity <= NS_debug && _check_debug_notify_protect) { // Someone issued a debug Notify output statement without protecting it // within an if statement. This can cause a significant runtime // performance hit, since it forces the iostream library to fully format @@ -172,36 +175,3 @@ update_severity_cache() { mark_cache_valid(_local_modified); } - -/** - * Returns the value of the notify-timestamp ConfigVariable. This is defined - * using a method accessor rather than a static ConfigVariableBool, to protect - * against the variable needing to be accessed at static init time. - */ -bool NotifyCategory:: -get_notify_timestamp() { - static ConfigVariableBool *notify_timestamp = nullptr; - if (notify_timestamp == nullptr) { - notify_timestamp = new ConfigVariableBool - ("notify-timestamp", false, - "Set true to output the date & time with each notify message."); - } - return *notify_timestamp; -} - -/** - * Returns the value of the check-debug-notify-protect ConfigVariable. This - * is defined using a method accessor rather than a static ConfigVariableBool, - * to protect against the variable needing to be accessed at static init time. - */ -bool NotifyCategory:: -get_check_debug_notify_protect() { - static ConfigVariableBool *check_debug_notify_protect = nullptr; - if (check_debug_notify_protect == nullptr) { - check_debug_notify_protect = new ConfigVariableBool - ("check-debug-notify-protect", false, - "Set true to issue a warning message if a debug or spam " - "notify output is not protected within an if statement."); - } - return *check_debug_notify_protect; -} diff --git a/dtool/src/prc/notifyCategory.h b/dtool/src/prc/notifyCategory.h index 96d2897921..1a0c82a37e 100644 --- a/dtool/src/prc/notifyCategory.h +++ b/dtool/src/prc/notifyCategory.h @@ -76,8 +76,6 @@ PUBLISHED: private: std::string get_config_name() const; void update_severity_cache(); - static bool get_notify_timestamp(); - static bool get_check_debug_notify_protect(); std::string _fullname; std::string _basename; @@ -86,12 +84,16 @@ private: typedef std::vector Children; Children _children; + static bool _notify_timestamp; + static bool _check_debug_notify_protect; + static long _server_delta; // not a time_t because server delta may be signed. AtomicAdjust::Integer _local_modified; NotifySeverity _severity_cache; friend class Notify; + friend class ConfigPageManager; }; INLINE std::ostream &operator << (std::ostream &out, const NotifyCategory &cat); From 557ce7a841930ab78e53e91ae952916a5e72e033 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 4 Sep 2025 13:32:10 +0200 Subject: [PATCH 121/291] express: Fix track-memory-usage with DeletedBufferChain off mark_pointer gets called twice, which is now legal --- panda/src/express/memoryUsage.cxx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/panda/src/express/memoryUsage.cxx b/panda/src/express/memoryUsage.cxx index 21161a025b..d5a7bb4a72 100644 --- a/panda/src/express/memoryUsage.cxx +++ b/panda/src/express/memoryUsage.cxx @@ -826,7 +826,8 @@ ns_record_void_pointer(void *ptr, size_t size) { if (_track_memory_usage) { if (express_cat.is_spam()) { express_cat.spam() - << "Recording void pointer " << (void *)ptr << "\n"; + << "Recording void pointer " << (void *)ptr + << " with size " << size << "\n"; } // We have to protect modifications to the table from recursive calls by @@ -846,11 +847,15 @@ ns_record_void_pointer(void *ptr, size_t size) { MemoryInfo *info = (*insert_result.first).second; - // We shouldn't already have a void pointer. + // If we already have a void pointer, that's okay, as long as the size + // matches. if (info->_void_ptr != nullptr) { - express_cat.error() - << "Void pointer " << (void *)ptr << " recorded twice!\n"; - nassertv(false); + if (info->_size != size) { + express_cat.error() + << "Void pointer " << (void *)ptr + << " was already recorded with size " << size << "!\n"; + nassertv(false); + } } if (info->_freeze_index == _freeze_index) { From b6dc37cfb0746fdc0652a9e59cf9a61ea1159c46 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 4 Sep 2025 13:46:56 +0200 Subject: [PATCH 122/291] pgraph: Fix CullableObject incorrect use of memory usage tracking --- panda/src/pgraph/cullResult.I | 6 +++++- panda/src/pgraph/cullResult.cxx | 3 +++ panda/src/pgraph/cullableObject.I | 16 ---------------- panda/src/pgraph/cullableObject.h | 2 +- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/panda/src/pgraph/cullResult.I b/panda/src/pgraph/cullResult.I index a0ea58eb3f..76e11fbd49 100644 --- a/panda/src/pgraph/cullResult.I +++ b/panda/src/pgraph/cullResult.I @@ -43,7 +43,11 @@ alloc_object(CullableObject &&object) { if (page->_size >= page->_capacity) { page = new_page(); } - return new (page->_memory + sizeof(CullableObject) * (page->_size++)) CullableObject(std::move(object)); + void *ptr = page->_memory + sizeof(CullableObject) * (page->_size++); +#ifdef DO_MEMORY_USAGE + //memory_hook->mark_pointer(ptr, sizeof(CullableObject), nullptr); +#endif + return new (ptr) CullableObject(std::move(object)); } /** diff --git a/panda/src/pgraph/cullResult.cxx b/panda/src/pgraph/cullResult.cxx index d4b1550935..0caa7231a7 100644 --- a/panda/src/pgraph/cullResult.cxx +++ b/panda/src/pgraph/cullResult.cxx @@ -385,6 +385,9 @@ delete_page(AllocationPage *page) { size_t size = std::exchange(page->_size, 0); for (size_t i = 0; i < size; ++i) { ((CullableObject *)page->_memory)[i].~CullableObject(); +#ifdef DO_MEMORY_USAGE + //MemoryUsage::remove_void_pointer(&((CullableObject *)page->_memory)[i]); +#endif } AllocationPage *next = page->_next; if (next != nullptr) { diff --git a/panda/src/pgraph/cullableObject.I b/panda/src/pgraph/cullableObject.I index 3910903948..6c1906bc79 100644 --- a/panda/src/pgraph/cullableObject.I +++ b/panda/src/pgraph/cullableObject.I @@ -11,16 +11,6 @@ * @date 2002-03-04 */ -/** - * Creates an empty CullableObject whose pointers can be filled in later. - */ -INLINE CullableObject:: -CullableObject() { -#ifdef DO_MEMORY_USAGE - MemoryUsage::record_pointer(this, get_class_type()); -#endif -} - /** * Creates a CullableObject based the indicated geom, with the indicated * render state and transform. @@ -32,9 +22,6 @@ CullableObject(CPT(Geom) geom, CPT(RenderState) state, _state(std::move(state)), _internal_transform(std::move(internal_transform)) { -#ifdef DO_MEMORY_USAGE - MemoryUsage::record_pointer(this, get_class_type()); -#endif } /** @@ -47,9 +34,6 @@ CullableObject(const CullableObject ©) : _state(copy._state), _internal_transform(copy._internal_transform) { -#ifdef DO_MEMORY_USAGE - MemoryUsage::record_pointer(this, get_class_type()); -#endif } /** diff --git a/panda/src/pgraph/cullableObject.h b/panda/src/pgraph/cullableObject.h index 46fdc228c6..26f1f7674f 100644 --- a/panda/src/pgraph/cullableObject.h +++ b/panda/src/pgraph/cullableObject.h @@ -40,7 +40,7 @@ class GeomMunger; */ class EXPCL_PANDA_PGRAPH CullableObject { public: - INLINE CullableObject(); + INLINE CullableObject() = default; INLINE CullableObject(CPT(Geom) geom, CPT(RenderState) state, CPT(TransformState) internal_transform); From 9427f2b590f49be48794618a16ed05addabfd2d3 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 4 Sep 2025 14:04:48 +0200 Subject: [PATCH 123/291] dtool: Track DeletedChain memory use even without USE_DELETED_CHAIN --- dtool/src/dtoolbase/deletedBufferChain.cxx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/dtool/src/dtoolbase/deletedBufferChain.cxx b/dtool/src/dtoolbase/deletedBufferChain.cxx index 9821129254..f4d269608b 100644 --- a/dtool/src/dtoolbase/deletedBufferChain.cxx +++ b/dtool/src/dtoolbase/deletedBufferChain.cxx @@ -88,7 +88,14 @@ allocate(size_t size, TypeHandle type_handle) { return ptr; #else // USE_DELETED_CHAIN - return PANDA_MALLOC_SINGLE(_buffer_size); + void *ptr = PANDA_MALLOC_SINGLE(_buffer_size); + +#ifdef DO_MEMORY_USAGE + type_handle.inc_memory_usage(TypeHandle::MC_singleton, _buffer_size); +#endif // DO_MEMORY_USAGE + + return ptr; + #endif // USE_DELETED_CHAIN } @@ -133,6 +140,11 @@ deallocate(void *ptr, TypeHandle type_handle) { _lock.unlock(); #else // USE_DELETED_CHAIN + +#ifdef DO_MEMORY_USAGE + type_handle.dec_memory_usage(TypeHandle::MC_singleton, _buffer_size); +#endif // DO_MEMORY_USAGE + PANDA_FREE_SINGLE(ptr); #endif // USE_DELETED_CHAIN } From d65e46e7b72964abffec7333134c29d318290c5f Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 4 Sep 2025 14:05:29 +0200 Subject: [PATCH 124/291] pgraph: Track CullableObject memory usage --- panda/src/pgraph/cullResult.cxx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/panda/src/pgraph/cullResult.cxx b/panda/src/pgraph/cullResult.cxx index 0caa7231a7..eb8d3519d2 100644 --- a/panda/src/pgraph/cullResult.cxx +++ b/panda/src/pgraph/cullResult.cxx @@ -65,7 +65,10 @@ CullResult(GraphicsStateGuardianBase *gsg, #ifdef DO_MEMORY_USAGE MemoryUsage::update_type(this, get_class_type()); -#endif + + CullableObject::get_class_type().inc_memory_usage( + TypeHandle::MC_array, sizeof(CullableObject) * AllocationPage::_capacity); +#endif // DO_MEMORY_USAGE #ifndef NDEBUG _show_transparency = show_transparency.get_value(); @@ -372,6 +375,10 @@ make_new_bin(int bin_index) { CullResult::AllocationPage *CullResult:: new_page() { AllocationPage *page = new AllocationPage; +#ifdef DO_MEMORY_USAGE + CullableObject::get_class_type().inc_memory_usage( + TypeHandle::MC_array, sizeof(CullableObject) * AllocationPage::_capacity); +#endif // DO_MEMORY_USAGE page->_next = _page; _page = page; return page; @@ -389,6 +396,12 @@ delete_page(AllocationPage *page) { //MemoryUsage::remove_void_pointer(&((CullableObject *)page->_memory)[i]); #endif } + +#ifdef DO_MEMORY_USAGE + CullableObject::get_class_type().dec_memory_usage( + TypeHandle::MC_array, sizeof(CullableObject) * AllocationPage::_capacity); +#endif // DO_MEMORY_USAGE + AllocationPage *next = page->_next; if (next != nullptr) { delete_page(next); From 3f45a8e367ff8964e1d0e5397ad4c61f322a1e08 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 4 Sep 2025 14:35:56 +0200 Subject: [PATCH 125/291] dtool: Improve memory usage tracking without USE_DELETED_CHAIN Now all classes that use ALLOC_DELETED_CHAIN participate in memory usage tracking even when USE_DELETED_CHAIN is not set (as long as DO_MEMORY_USAGE is set) --- dtool/src/dtoolbase/deletedChain.h | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/dtool/src/dtoolbase/deletedChain.h b/dtool/src/dtoolbase/deletedChain.h index a1b08a31bf..f418d4ee59 100644 --- a/dtool/src/dtoolbase/deletedChain.h +++ b/dtool/src/dtoolbase/deletedChain.h @@ -122,6 +122,50 @@ public: #define ALLOC_DELETED_CHAIN_DEF(Type) \ DeletedChain< Type > Type::_deleted_chain; +#elif defined(DO_MEMORY_USAGE) + +#define ALLOC_DELETED_CHAIN(Type) \ + inline void *operator new(size_t size) RETURNS_ALIGNED(MEMORY_HOOK_ALIGNMENT) { \ + void *ptr = PANDA_MALLOC_SINGLE(size); \ + get_type_handle(Type).inc_memory_usage(TypeHandle::MC_singleton, sizeof(Type)); \ + return ptr; \ + } \ + inline void *operator new(size_t size, void *ptr) { \ + (void) size; \ + return ptr; \ + } \ + inline void operator delete(void *ptr) { \ + if (ptr != nullptr) { \ + get_type_handle(Type).dec_memory_usage(TypeHandle::MC_singleton, sizeof(Type)); \ + PANDA_FREE_SINGLE(ptr); \ + } \ + } \ + inline void operator delete(void *, void *) { \ + } \ + inline void *operator new[](size_t size) RETURNS_ALIGNED(MEMORY_HOOK_ALIGNMENT) { \ + void *ptr = PANDA_MALLOC_SINGLE(size); \ + get_type_handle(Type).inc_memory_usage(TypeHandle::MC_array, sizeof(Type)); \ + return ptr; \ + } \ + inline void *operator new[](size_t size, void *ptr) { \ + (void) size; \ + return ptr; \ + } \ + inline void operator delete[](void *ptr) { \ + if (ptr != nullptr) { \ + get_type_handle(Type).dec_memory_usage(TypeHandle::MC_array, sizeof(Type)); \ + PANDA_FREE_SINGLE(ptr); \ + } \ + } \ + inline void operator delete[](void *, void *) { \ + } \ + inline static bool validate_ptr(const void *ptr) { \ + return (ptr != nullptr); \ + } + +#define ALLOC_DELETED_CHAIN_DECL(Type) ALLOC_DELETED_CHAIN(Type) +#define ALLOC_DELETED_CHAIN_DEF(Type) + #else // USE_DELETED_CHAIN #define ALLOC_DELETED_CHAIN(Type) \ From cd791ca7fc969357a8d7bcc54d3b2685a9c88af7 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 4 Sep 2025 16:28:11 +0200 Subject: [PATCH 126/291] pstats: Add new feature to count Python objects by type This is useful for profiling cases where the number of Python objects is growing more than expected Requires Python 3.14a6 and can be enabled with pstats-python-ref-tracer true --- dtool/src/parser-inc/Python.h | 5 + panda/src/pstatclient/config_pstatclient.cxx | 10 ++ panda/src/pstatclient/config_pstatclient.h | 1 + panda/src/pstatclient/pStatClient.h | 3 + panda/src/pstatclient/pStatClient_ext.cxx | 138 ++++++++++++++++++- panda/src/pstatclient/pStatClient_ext.h | 4 + 6 files changed, 160 insertions(+), 1 deletion(-) diff --git a/dtool/src/parser-inc/Python.h b/dtool/src/parser-inc/Python.h index ec9ad04008..f3e76bb568 100644 --- a/dtool/src/parser-inc/Python.h +++ b/dtool/src/parser-inc/Python.h @@ -52,4 +52,9 @@ PyObject _Py_FalseStruct; typedef void *visitproc; +typedef enum { + PyRefTracer_CREATE = 0, + PyRefTracer_DESTROY = 1, +} PyRefTracerEvent; + #endif // PYTHON_H diff --git a/panda/src/pstatclient/config_pstatclient.cxx b/panda/src/pstatclient/config_pstatclient.cxx index 640a4777d5..54d9398dce 100644 --- a/panda/src/pstatclient/config_pstatclient.cxx +++ b/panda/src/pstatclient/config_pstatclient.cxx @@ -98,6 +98,16 @@ ConfigVariableBool pstats_python_profiler "somewhat, and requires a recent version of the PStats server, so " "it is not enabled by default.")); +ConfigVariableBool pstats_python_ref_tracer +("pstats-python-ref-tracer", false, + PRC_DESC("Set this true to integrate with the Python ref tracer to show a " + "count of all Python objects by module. This is not enabled by " + "default since it incurs a significant performance overhead, but " + "can be useful to find bottlenecks caused by a growth of objects " + "in the Python interpreter. This feature is only available as of " + "Python 3.14a6.")); + + // The rest are different in that they directly control the server, not the // client. ConfigVariableBool pstats_scroll_mode diff --git a/panda/src/pstatclient/config_pstatclient.h b/panda/src/pstatclient/config_pstatclient.h index a16967c964..fd83a22f40 100644 --- a/panda/src/pstatclient/config_pstatclient.h +++ b/panda/src/pstatclient/config_pstatclient.h @@ -40,6 +40,7 @@ extern EXPCL_PANDA_PSTATCLIENT ConfigVariableDouble pstats_target_frame_rate; extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_gpu_timing; extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_thread_profiling; extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_python_profiler; +extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_python_ref_tracer; extern EXPCL_PANDA_PSTATCLIENT ConfigVariableBool pstats_scroll_mode; extern EXPCL_PANDA_PSTATCLIENT ConfigVariableDouble pstats_history; diff --git a/panda/src/pstatclient/pStatClient.h b/panda/src/pstatclient/pStatClient.h index 97139e770d..4548d29d01 100644 --- a/panda/src/pstatclient/pStatClient.h +++ b/panda/src/pstatclient/pStatClient.h @@ -234,6 +234,9 @@ private: size_t _threads_size {0}; // size of the allocated array patomic _num_threads {0}; // number of in-use elements within the array + // See pStatClient_ext.cxx. + pmap _python_type_collectors; + mutable PStatClientImpl *_impl; static PStatCollector _heap_total_size_pcollector; diff --git a/panda/src/pstatclient/pStatClient_ext.cxx b/panda/src/pstatclient/pStatClient_ext.cxx index f786b07dcd..0f1418a1d3 100644 --- a/panda/src/pstatclient/pStatClient_ext.cxx +++ b/panda/src/pstatclient/pStatClient_ext.cxx @@ -17,6 +17,7 @@ #include "pStatCollector.h" #include "config_pstatclient.h" +#include "reMutexHolder.h" #ifndef CPPPARSER #include "frameobject.h" @@ -33,6 +34,9 @@ static pmap _c_method_collectors; // Parent collector for all Python profiling collectors. static PStatCollector code_collector("App:Python"); +// Parent collector for all Python ref tracer collectors. +static PStatCollector refs_collector("Python objects"); + /** * Walks up the type hierarchy to find the class where the method originates. */ @@ -263,8 +267,25 @@ client_connect(std::string hostname, int port) { _extra_index = _PyEval_RequestCodeExtraIndex(nullptr); } PyEval_SetProfile(&trace_callback, arg); - _python_profiler_enabled = false; + _python_profiler_enabled = true; } + + // We require 3.14a6, since that version fixes an important bug with the + // ref tracer; prior versions did not properly send destroy events. +#if PY_VERSION_HEX >= 0x030E0000 + if (Py_Version >= 0x030E00A6) { + if (pstats_python_ref_tracer) { + PyRefTracer_SetTracer(&ref_trace_callback, _this); + } + } + else +#endif + if (pstats_python_ref_tracer) { + pstats_cat.warning() + << "The pstats-python-ref-tracer feature requires at least " + "Python 3.14a6.\n"; + } + return true; } else if (_python_profiler_enabled) { @@ -284,6 +305,13 @@ client_disconnect() { PyEval_SetProfile(nullptr, nullptr); _python_profiler_enabled = false; } + +#if PY_VERSION_HEX >= 0x030E0000 // 3.14 + void *data; + if (PyRefTracer_GetTracer(&data) == &ref_trace_callback && data == _this) { + PyRefTracer_SetTracer(nullptr, nullptr); + } +#endif } /** @@ -365,4 +393,112 @@ trace_callback(PyObject *py_thread, PyFrameObject *frame, int what, PyObject *ar return 0; } +/** + * Callback passed to PyRefTracer_SetTracer. + */ +#if PY_VERSION_HEX >= 0x030E0000 // 3.14 +int Extension:: +ref_trace_callback(PyObject *obj, PyRefTracerEvent event, void *data) { + PStatClient *client = (PStatClient *)data; + if (!client->client_is_connected()) { + return 0; + } + + PyTypeObject *cls = Py_TYPE(obj); + +#ifdef Py_GIL_DISABLED + // With GIL disabled, the GIL is no longer protecting the cache, so we + // have to do that ourselves. + client->_lock.acquire(); +#endif + + int collector_index; + auto it = client->_python_type_collectors.find(cls); + if (it != client->_python_type_collectors.end()) { + collector_index = it->second; +#ifdef Py_GIL_DISABLED + client->_lock.release(); +#endif + } + else { +#ifdef Py_GIL_DISABLED + client->_lock.release(); +#endif + char buffer[1024]; + size_t len; + + if (cls == &PyDict_Type || cls == &PyUnicode_Type) { + // Prevents recursion due to PyDict_GetItemStringRef + len = snprintf(buffer, sizeof(buffer), "builtins:%s", cls->tp_name); + } + else { + const char *dot = strrchr(cls->tp_name, '.'); + if (dot != nullptr) { + // The module name is included in the type name. + len = snprintf(buffer, sizeof(buffer), "%s", cls->tp_name); + } else { + // If there's no module name, we need to get it from __module__. + PyObject *py_mod_name = nullptr; + const char *mod_name = nullptr; + if (cls->tp_dict != nullptr && + PyDict_GetItemStringRef(cls->tp_dict, "__module__", &py_mod_name) > 0) { + if (PyUnicode_Check(py_mod_name)) { + mod_name = PyUnicode_AsUTF8(py_mod_name); + } else { + // Might be a descriptor. + Py_DECREF(py_mod_name); + py_mod_name = PyObject_GetAttrString(obj, "__module__"); + if (py_mod_name != nullptr) { + if (PyUnicode_Check(py_mod_name)) { + mod_name = PyUnicode_AsUTF8(py_mod_name); + } + } + else PyErr_Clear(); + } + } + else PyErr_Clear(); + + if (mod_name == nullptr) { + // Is it a built-in, like int or dict? + PyObject *builtins = PyEval_GetBuiltins(); + if (PyDict_GetItemString(builtins, cls->tp_name) == (PyObject *)cls) { + mod_name = "builtins"; + } else { + mod_name = ""; + } + } + len = snprintf(buffer, sizeof(buffer), "%s:%s", mod_name, cls->tp_name); + Py_XDECREF(py_mod_name); + } + } + + for (size_t i = 0; i < len; ++i) { + if (buffer[i] == '.') { + buffer[i] = ':'; + } + } + + std::string collector_name(buffer, len); + +#ifdef Py_GIL_DISABLED + ReMutexHolder holder(client->_lock); +#endif + collector_index = client->make_collector_with_relname(refs_collector.get_index(), collector_name).get_index(); + client->_python_type_collectors[cls] = collector_index; + } + + switch (event) { + case PyRefTracer_CREATE: + client->add_level(collector_index, 0, 1); + break; + + case PyRefTracer_DESTROY: + client->add_level(collector_index, 0, -1); + break; + } + + return 0; +} +#endif + #endif // HAVE_PYTHON && DO_PSTATS diff --git a/panda/src/pstatclient/pStatClient_ext.h b/panda/src/pstatclient/pStatClient_ext.h index 216d7777b1..03628d66e5 100644 --- a/panda/src/pstatclient/pStatClient_ext.h +++ b/panda/src/pstatclient/pStatClient_ext.h @@ -40,6 +40,10 @@ public: private: static int trace_callback(PyObject *py_thread, PyFrameObject *frame, int what, PyObject *arg); + +#if PY_VERSION_HEX >= 0x030E0000 // 3.14 + static int ref_trace_callback(PyObject *obj, PyRefTracerEvent event, void *data); +#endif }; #include "pStatClient_ext.I" From b32d5c672441c280f7e799483d223e252cf9f797 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 5 Sep 2025 09:59:25 +0200 Subject: [PATCH 127/291] pstats: Enable ref tracing feature in Python 3.13.3+ as well The fix to Python was backported here. --- panda/src/pstatclient/pStatClient_ext.cxx | 14 +++++++------- panda/src/pstatclient/pStatClient_ext.h | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/panda/src/pstatclient/pStatClient_ext.cxx b/panda/src/pstatclient/pStatClient_ext.cxx index 0f1418a1d3..588fe8c1c1 100644 --- a/panda/src/pstatclient/pStatClient_ext.cxx +++ b/panda/src/pstatclient/pStatClient_ext.cxx @@ -270,10 +270,10 @@ client_connect(std::string hostname, int port) { _python_profiler_enabled = true; } - // We require 3.14a6, since that version fixes an important bug with the - // ref tracer; prior versions did not properly send destroy events. -#if PY_VERSION_HEX >= 0x030E0000 - if (Py_Version >= 0x030E00A6) { + // We require 3.13.3 or 3.14a6, since those versions fix an important bug + // with the ref tracer; prior versions did not always send destroy events. +#if PY_VERSION_HEX >= 0x030D0000 + if (Py_Version >= 0x030D0300 || Py_Version >= 0x030E00A6) { if (pstats_python_ref_tracer) { PyRefTracer_SetTracer(&ref_trace_callback, _this); } @@ -283,7 +283,7 @@ client_connect(std::string hostname, int port) { if (pstats_python_ref_tracer) { pstats_cat.warning() << "The pstats-python-ref-tracer feature requires at least " - "Python 3.14a6.\n"; + "Python 3.13.3 or Python 3.14a6.\n"; } return true; @@ -306,7 +306,7 @@ client_disconnect() { _python_profiler_enabled = false; } -#if PY_VERSION_HEX >= 0x030E0000 // 3.14 +#if PY_VERSION_HEX >= 0x030D0000 // 3.13 void *data; if (PyRefTracer_GetTracer(&data) == &ref_trace_callback && data == _this) { PyRefTracer_SetTracer(nullptr, nullptr); @@ -396,7 +396,7 @@ trace_callback(PyObject *py_thread, PyFrameObject *frame, int what, PyObject *ar /** * Callback passed to PyRefTracer_SetTracer. */ -#if PY_VERSION_HEX >= 0x030E0000 // 3.14 +#if PY_VERSION_HEX >= 0x030D0000 // 3.13 int Extension:: ref_trace_callback(PyObject *obj, PyRefTracerEvent event, void *data) { PStatClient *client = (PStatClient *)data; diff --git a/panda/src/pstatclient/pStatClient_ext.h b/panda/src/pstatclient/pStatClient_ext.h index 03628d66e5..47c43b178d 100644 --- a/panda/src/pstatclient/pStatClient_ext.h +++ b/panda/src/pstatclient/pStatClient_ext.h @@ -41,7 +41,7 @@ private: static int trace_callback(PyObject *py_thread, PyFrameObject *frame, int what, PyObject *arg); -#if PY_VERSION_HEX >= 0x030E0000 // 3.14 +#if PY_VERSION_HEX >= 0x030D0000 // 3.13 static int ref_trace_callback(PyObject *obj, PyRefTracerEvent event, void *data); #endif }; From 70c6251c768f95f32f44dea734b05b4660fd8a1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:15:19 +0000 Subject: [PATCH 128/291] build(deps): bump actions/setup-python from 5 to 6 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 22 +++++++++++----------- .github/workflows/mypy.yml | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b1e447057..7ee75a67f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -193,7 +193,7 @@ jobs: - name: Setup Python (Python 3.8) if: contains(matrix.python, 'YES') - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.8' - name: Configure (Python 3.8) @@ -225,7 +225,7 @@ jobs: - name: Setup Python (Python 3.9) if: contains(matrix.python, 'YES') - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.9' - name: Configure (Python 3.9) @@ -257,7 +257,7 @@ jobs: - name: Setup Python (Python 3.10) if: contains(matrix.python, 'YES') - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: Configure (Python 3.10) @@ -289,7 +289,7 @@ jobs: - name: Setup Python (Python 3.11) if: contains(matrix.python, 'YES') - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' - name: Configure (Python 3.11) @@ -321,7 +321,7 @@ jobs: - name: Setup Python (Python 3.12) if: contains(matrix.python, 'YES') - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' - name: Configure (Python 3.12) @@ -403,7 +403,7 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer - name: Set up Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.13' - name: Build Python 3.13 @@ -417,7 +417,7 @@ jobs: PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest - name: Set up Python 3.12 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' - name: Build Python 3.12 (double) @@ -435,7 +435,7 @@ jobs: python makepanda/makepackage.py --verbose --lzma --outputdir=built_dbl - name: Set up Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' - name: Build Python 3.11 @@ -449,7 +449,7 @@ jobs: PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest - name: Set up Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: Build Python 3.10 @@ -463,7 +463,7 @@ jobs: PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest - name: Set up Python 3.9 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.9' - name: Build Python 3.9 @@ -477,7 +477,7 @@ jobs: PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest - name: Set up Python 3.8 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.8' - name: Build Python 3.8 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 40e4cdcf46..5b2869902c 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From b275a72d0f6e473a9cb70ae2faa0d7b942f387cf Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 14 Sep 2025 09:59:05 +0200 Subject: [PATCH 129/291] pstats: Fix version check for ref tracing feature --- panda/src/pstatclient/pStatClient_ext.cxx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/panda/src/pstatclient/pStatClient_ext.cxx b/panda/src/pstatclient/pStatClient_ext.cxx index 588fe8c1c1..dfe94536b5 100644 --- a/panda/src/pstatclient/pStatClient_ext.cxx +++ b/panda/src/pstatclient/pStatClient_ext.cxx @@ -273,7 +273,12 @@ client_connect(std::string hostname, int port) { // We require 3.13.3 or 3.14a6, since those versions fix an important bug // with the ref tracer; prior versions did not always send destroy events. #if PY_VERSION_HEX >= 0x030D0000 - if (Py_Version >= 0x030D0300 || Py_Version >= 0x030E00A6) { +#if PY_VERSION_HEX >= 0x030E0000 + if (Py_Version >= 0x030E00A6) +#else + if (Py_Version >= 0x030D0300) +#endif + { if (pstats_python_ref_tracer) { PyRefTracer_SetTracer(&ref_trace_callback, _this); } From e661d028156b9111e2a54c064c479cc8e60ae203 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 14 Sep 2025 11:00:13 +0200 Subject: [PATCH 130/291] glgsg: Fix sharing shaders between GSGs Fixes #1728 --- panda/src/glstuff/glCgShaderContext_src.cxx | 7 +++++-- panda/src/glstuff/glCgShaderContext_src.h | 2 +- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 2 +- panda/src/glstuff/glShaderContext_src.cxx | 13 ++++++++----- panda/src/glstuff/glShaderContext_src.h | 2 +- panda/src/gobj/shaderContext.h | 4 +++- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/panda/src/glstuff/glCgShaderContext_src.cxx b/panda/src/glstuff/glCgShaderContext_src.cxx index 50e72f86a4..299ee1cab7 100644 --- a/panda/src/glstuff/glCgShaderContext_src.cxx +++ b/panda/src/glstuff/glCgShaderContext_src.cxx @@ -388,14 +388,17 @@ valid() { * all of the shader's input parameters. */ void CLP(CgShaderContext):: -bind() { +bind(GraphicsStateGuardian *gsg) { + CLP(GraphicsStateGuardian) *glgsg = (CLP(GraphicsStateGuardian) *)gsg; + _glgsg = glgsg; + if (_cg_program != 0) { // Bind the shaders. cgGLEnableProgramProfiles(_cg_program); cgGLBindProgram(_cg_program); cg_report_errors(); - _glgsg->report_my_gl_errors(); + glgsg->report_my_gl_errors(); } } diff --git a/panda/src/glstuff/glCgShaderContext_src.h b/panda/src/glstuff/glCgShaderContext_src.h index a9b4157c6e..97c11fe253 100644 --- a/panda/src/glstuff/glCgShaderContext_src.h +++ b/panda/src/glstuff/glCgShaderContext_src.h @@ -34,7 +34,7 @@ public: ALLOC_DELETED_CHAIN(CLP(CgShaderContext)); bool valid(void) override; - void bind() override; + void bind(GraphicsStateGuardian *gsg) override; void unbind() override; void set_state_and_transform(const RenderState *state, diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 6ba7a5e01d..ba9372e45e 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -8717,7 +8717,7 @@ do_issue_shader() { // If it's a different type of shader, make sure to unbind the old. _current_shader_context->unbind(); } - context->bind(); + context->bind(this); _current_shader = shader; } diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx index 169ae1b670..9826fad4bb 100644 --- a/panda/src/glstuff/glShaderContext_src.cxx +++ b/panda/src/glstuff/glShaderContext_src.cxx @@ -416,7 +416,7 @@ CLP(ShaderContext)(CLP(GraphicsStateGuardian) *glgsg, Shader *s) : ShaderContext if (_glgsg->_current_shader_context == nullptr) { _glgsg->_glUseProgram(0); } else { - _glgsg->_current_shader_context->bind(); + _glgsg->_current_shader_context->bind(_glgsg); } _mat_part_cache = new LVecBase4f[_shader->cp_get_mat_cache_size()]; @@ -2192,15 +2192,18 @@ valid() { * all of the shader's input parameters. */ void CLP(ShaderContext):: -bind() { +bind(GraphicsStateGuardian *gsg) { + CLP(GraphicsStateGuardian) *glgsg = (CLP(GraphicsStateGuardian) *)gsg; + _glgsg = glgsg; + if (!_validated) { - _glgsg->_glValidateProgram(_glsl_program); + glgsg->_glValidateProgram(_glsl_program); glsl_report_program_errors(_glsl_program, false); _validated = true; } if (!_shader->get_error_flag()) { - _glgsg->_glUseProgram(_glsl_program); + glgsg->_glUseProgram(_glsl_program); } if (GLCAT.is_spam()) { @@ -2208,7 +2211,7 @@ bind() { << _shader->get_filename() << "\n"; } - _glgsg->report_my_gl_errors(); + glgsg->report_my_gl_errors(); } /** diff --git a/panda/src/glstuff/glShaderContext_src.h b/panda/src/glstuff/glShaderContext_src.h index 3088170ec3..e65355e3a0 100644 --- a/panda/src/glstuff/glShaderContext_src.h +++ b/panda/src/glstuff/glShaderContext_src.h @@ -42,7 +42,7 @@ public: bool get_sampler_texture_type(int &out, GLenum param_type); bool valid(void) override; - void bind() override; + void bind(GraphicsStateGuardian *gsg) override; void unbind() override; void set_state_and_transform(const RenderState *state, diff --git a/panda/src/gobj/shaderContext.h b/panda/src/gobj/shaderContext.h index a8d8add8ed..53fb69bce6 100644 --- a/panda/src/gobj/shaderContext.h +++ b/panda/src/gobj/shaderContext.h @@ -19,6 +19,8 @@ #include "savedContext.h" #include "shader.h" +class GraphicsStateGuardian; + /** * The ShaderContext is meant to contain the compiled version of a shader * string. ShaderContext is an abstract base class, there will be a subclass @@ -38,7 +40,7 @@ public: const TransformState *) {}; INLINE virtual bool valid() { return false; } - INLINE virtual void bind() {}; + INLINE virtual void bind(GraphicsStateGuardian *gsg) {}; INLINE virtual void unbind() {}; INLINE virtual void issue_parameters(int altered) {}; INLINE virtual void disable_shader_vertex_arrays() {}; From e68a3db7c9b603095176430ff2b5992961a4330c Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 6 Oct 2025 21:01:20 +0200 Subject: [PATCH 131/291] glgsg: Fix typo causing SSBO support not always detected --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index ba9372e45e..6865b54a16 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -2438,7 +2438,7 @@ reset() { #ifndef OPENGLES // Check for SSBOs. - if (is_at_least_gl_version(4, 3) || has_extension("ARB_shader_storage_buffer_object")) { + if (is_at_least_gl_version(4, 3) || has_extension("GL_ARB_shader_storage_buffer_object")) { _supports_shader_buffers = true; _glGetProgramInterfaceiv = (PFNGLGETPROGRAMINTERFACEIVPROC) get_extension_func("glGetProgramInterfaceiv"); From 77fa04adb0148522b310a9d60b895009313cd4af Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 6 Oct 2025 21:01:20 +0200 Subject: [PATCH 132/291] glgsg: Fix typo causing SSBO support not always detected --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 1d92309a65..a11e03377e 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -2146,7 +2146,7 @@ reset() { #ifndef OPENGLES // Check for SSBOs. - if (is_at_least_gl_version(4, 3) || has_extension("ARB_shader_storage_buffer_object")) { + if (is_at_least_gl_version(4, 3) || has_extension("GL_ARB_shader_storage_buffer_object")) { _supports_shader_buffers = true; _glGetProgramInterfaceiv = (PFNGLGETPROGRAMINTERFACEIVPROC) get_extension_func("glGetProgramInterfaceiv"); From 2540648ac9729e79c336f8c44df4eee58d897933 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 10 Oct 2025 14:11:42 +0200 Subject: [PATCH 133/291] nativenet: Change deprecated sprintf to snprintf --- dtool/LocalSetup.cmake | 1 + panda/src/nativenet/socket_address.cxx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dtool/LocalSetup.cmake b/dtool/LocalSetup.cmake index b92fb8614c..5ddea90f9a 100644 --- a/dtool/LocalSetup.cmake +++ b/dtool/LocalSetup.cmake @@ -133,6 +133,7 @@ check_include_file_cxx(dirent.h PHAVE_DIRENT_H) check_include_file_cxx(ucontext.h PHAVE_UCONTEXT_H) #TODO doesn't work on OSX, use sys/ucontext.h check_include_file_cxx(linux/input.h PHAVE_LINUX_INPUT_H) check_include_file_cxx(stdint.h PHAVE_STDINT_H) +check_include_file_cxx(execinfo.h PHAVE_EXECINFO_H) # Do we have Posix threads? #set(HAVE_POSIX_THREADS ${CMAKE_USE_PTHREADS_INIT}) diff --git a/panda/src/nativenet/socket_address.cxx b/panda/src/nativenet/socket_address.cxx index e92310ce7f..96a9ece619 100644 --- a/panda/src/nativenet/socket_address.cxx +++ b/panda/src/nativenet/socket_address.cxx @@ -125,13 +125,15 @@ get_ip_port() const { if (_storage.ss_family == AF_INET) { getnameinfo(&_addr, sizeof(sockaddr_in), buf, sizeof(buf), nullptr, 0, NI_NUMERICHOST); - sprintf(buf + strlen(buf), ":%hu", get_port()); + size_t len = strlen(buf); + snprintf(buf + len, sizeof(buf) - len, ":%hu", get_port()); } else if (_storage.ss_family == AF_INET6) { // Protect the IPv6 address within square brackets. buf[0] = '['; getnameinfo(&_addr, sizeof(sockaddr_in6), buf + 1, sizeof(buf) - 1, nullptr, 0, NI_NUMERICHOST); - sprintf(buf + strlen(buf), "]:%hu", get_port()); + size_t len = strlen(buf); + snprintf(buf + len, sizeof(buf) - len, "]:%hu", get_port()); } else { nassert_raise("unsupported address family"); From 6ac2164c2575e9e6badea3f3747a91fea67d4940 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 10 Oct 2025 14:12:33 +0200 Subject: [PATCH 134/291] notify: Print out C++ backtrace on assert-abort --- dtool/src/parser-inc/DbgHelp.h | 0 dtool/src/prc/notify.cxx | 185 ++++++++++++++++++++++++++++++++- dtool/src/prc/pnotify.h | 18 ++-- makepanda/makepanda.py | 3 + 4 files changed, 193 insertions(+), 13 deletions(-) create mode 100644 dtool/src/parser-inc/DbgHelp.h diff --git a/dtool/src/parser-inc/DbgHelp.h b/dtool/src/parser-inc/DbgHelp.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dtool/src/prc/notify.cxx b/dtool/src/prc/notify.cxx index ce7df151c6..5c4010264b 100644 --- a/dtool/src/prc/notify.cxx +++ b/dtool/src/prc/notify.cxx @@ -35,6 +35,18 @@ #include "emscriptenLogStream.h" #endif +#ifndef NDEBUG +#ifdef PHAVE_EXECINFO_H +#include // for backtrace() +#include +#include +#endif + +#ifdef _WIN32 +#include +#endif +#endif // NDEBUG + using std::cerr; using std::cout; using std::ostream; @@ -348,15 +360,16 @@ assert_failure(const char *expression, int line, << expression << " at line " << line << " of " << source_file; string message = message_str.str(); - if (!_assert_failed) { + Notify *self = ptr(); + if (!self->_assert_failed) { // We only save the first assertion failure message, as this is usually // the most meaningful when several occur in a row. - _assert_failed = true; - _assert_error_message = message; + self->_assert_failed = true; + self->_assert_error_message = message; } - if (has_assert_handler()) { - return (*_assert_handler)(expression, line, source_file); + if (self->has_assert_handler()) { + return (*self->_assert_handler)(expression, line, source_file); } #ifdef ANDROID @@ -380,6 +393,33 @@ assert_failure(const char *expression, int line, // Make sure the error message has been flushed to the output. nout.flush(); + // Capture and list a stack trace. +#ifdef NDEBUG +#elif defined(PHAVE_EXECINFO_H) + void *trace[64]; + int size = backtrace(trace, 64); + if (size > 0) { + // Remove the frame(s) corresponding to the current function. + void **tracep = trace; + void *return_addr = __builtin_return_address(0); + for (int i = 0; i < size; ++i) { + if (trace[i] == return_addr) { + tracep = trace + (i + 1); + size -= i + 1; + break; + } + } + write_backtrace(tracep, size); + } +#elif defined(_WIN32) + const ULONG max_size = 62; + void *trace[max_size]; + int size = CaptureStackBackTrace(1, max_size, trace, nullptr); + if (size > 0) { + write_backtrace(trace, size); + } +#endif + #ifdef _MSC_VER // How to trigger an exception in VC++ that offers to take us into the // debugger? abort() doesn't do it. We used to be able to assert(false), @@ -411,6 +451,141 @@ assert_failure(const char *expression, int line, return true; } +/** + * + */ +void Notify:: +write_backtrace(void **trace, int size) { + std::ostream &out = nout; + +#ifdef NDEBUG +#elif defined(PHAVE_EXECINFO_H) + char namebuf[128]; + + for (int i = 0; i < size; ++i) { + void *addr = trace[i]; + Dl_info info; + if (dladdr((char *)addr - 1, &info) != 0) { + const char *name = nullptr; + + if (info.dli_sname != nullptr) { + int status = 0; + size_t size = 0; + char *demangled = nullptr; + if (info.dli_sname[0] == '_') { + size = sizeof(namebuf) - 1; + demangled = abi::__cxa_demangle(info.dli_sname, namebuf, &size, &status); + } + if (status == 0 && demangled != nullptr) { + namebuf[size] = 0; + name = demangled; + //if (strncmp(name, "Notify::assert_failure", 22) == 0) { + //continue; + //} + } else { + name = info.dli_sname; + } + } + + out << "[" << addr << "] "; + + if (info.dli_fname != nullptr) { + const char *slash = strrchr(info.dli_fname, '/'); + out << std::left << std::setw(30) << (slash ? slash + 1 : info.dli_fname) << " "; + } + + if (name != nullptr) { + out << name; + } else { + out << info.dli_saddr; + } + + int offset = reinterpret_cast(addr) - reinterpret_cast(info.dli_saddr); + if (offset > 0) { + out << " + " << offset; + } + out << "\n"; + } else { + out << "[" << addr << "]\n"; + } + } +#elif defined(_WIN32) + HMODULE handle = LoadLibraryA("dbghelp.dll"); + if (!handle) { + return; + } + + auto pSymInitialize = (BOOL (WINAPI *)(HANDLE, PCSTR, BOOL))GetProcAddress(handle, "SymInitialize"); + auto pSymCleanup = (BOOL (WINAPI *)(HANDLE))GetProcAddress(handle, "SymCleanup"); + auto pSymFromAddr = (BOOL (WINAPI *)(HANDLE, DWORD64, DWORD64 *, PSYMBOL_INFO))GetProcAddress(handle, "SymFromAddr"); + auto pSymGetModuleInfo64 = (BOOL (WINAPI *)(HANDLE, DWORD64, PIMAGEHLP_MODULE64))GetProcAddress(handle, "SymGetModuleInfo64"); + auto pSymGetLineFromAddr64 = (BOOL (WINAPI *)(HANDLE, DWORD64, PDWORD, PIMAGEHLP_LINE64))GetProcAddress(handle, "SymGetLineFromAddr64"); + if (!pSymInitialize || !pSymCleanup || !pSymFromAddr) { + FreeLibrary(handle); + return; + } + + HANDLE process = GetCurrentProcess(); + pSymInitialize(process, nullptr, TRUE); + + for (int i = 0; i < size; ++i) { + DWORD64 addr = (DWORD64)trace[i]; + + alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + 256] = {0}; + SYMBOL_INFO *symbol = (SYMBOL_INFO *)buffer; + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + symbol->MaxNameLen = 255; + + out << "[" << (void *)addr << "]"; + + // Show the name of the library. + IMAGEHLP_MODULE64 module; + module.SizeOfStruct = sizeof(module); + const char *basename = "???"; + if (pSymGetModuleInfo64 && pSymGetModuleInfo64(process, addr, &module)) { + const char *slash = strrchr(module.ImageName, '\\'); + basename = (slash ? slash + 1 : module.ImageName); + } + + // Look up the symbol name. If the reported name comes from the export + // table and the address is way off, ignore it, it's probably not right. + if (pSymFromAddr(process, addr, nullptr, symbol) && + ((symbol->Flags & SYMFLAG_EXPORT) == 0 || addr - (DWORD64)symbol->Address < 0x4000)) { + + out << " " << std::left << std::setw(30) << basename << " " << symbol->Name; + + DWORD64 offset = addr - (DWORD64)(symbol->Address); + if (offset > 0) { + out << " + " << offset; + } + + // Look up filename + line number if available. + IMAGEHLP_LINE64 line; + DWORD displacement = 0; + line.SizeOfStruct = sizeof(line); + if (pSymGetLineFromAddr64(process, addr, &displacement, &line)) { + const char *slash = strrchr(line.FileName, '\\'); + const char *basename = (slash ? slash + 1 : line.FileName); + + out << " (" << basename << ":" << line.LineNumber; + //if (displacement > 0) { + // out << " + " << displacement; + //} + out << ")"; + } + } else { + out << " " << basename; + } + out << "\n"; + } + + pSymCleanup(process); + FreeLibrary(handle); +#endif + + out.flush(); +} + /** * Given a string, one of "debug", "info", "warning", etc., return the * corresponding Severity level, or NS_unspecified if none of the strings diff --git a/dtool/src/prc/pnotify.h b/dtool/src/prc/pnotify.h index a4c9a6aca2..76ab5be7fb 100644 --- a/dtool/src/prc/pnotify.h +++ b/dtool/src/prc/pnotify.h @@ -76,10 +76,12 @@ PUBLISHED: public: static ios_fmtflags get_literal_flag(); - bool assert_failure(const std::string &expression, int line, - const char *source_file); - bool assert_failure(const char *expression, int line, - const char *source_file); + static bool assert_failure(const std::string &expression, int line, + const char *source_file); + static bool assert_failure(const char *expression, int line, + const char *source_file); + + static void write_backtrace(void **trace, int size); static NotifySeverity string_severity(const std::string &string); @@ -204,7 +206,7 @@ private: #define nassertr(condition, return_value) \ { \ if (_nassert_check(condition)) { \ - if (Notify::ptr()->assert_failure(#condition, __LINE__, __FILE__)) { \ + if (Notify::assert_failure(#condition, __LINE__, __FILE__)) { \ return return_value; \ } \ } \ @@ -213,7 +215,7 @@ private: #define nassertv(condition) \ { \ if (_nassert_check(condition)) { \ - if (Notify::ptr()->assert_failure(#condition, __LINE__, __FILE__)) { \ + if (Notify::assert_failure(#condition, __LINE__, __FILE__)) { \ return; \ } \ } \ @@ -221,12 +223,12 @@ private: #define nassertd(condition) \ if (_nassert_check(condition) && \ - Notify::ptr()->assert_failure(#condition, __LINE__, __FILE__)) + Notify::assert_failure(#condition, __LINE__, __FILE__)) #define nassertr_always(condition, return_value) nassertr(condition, return_value) #define nassertv_always(condition) nassertv(condition) -#define nassert_raise(message) Notify::ptr()->assert_failure(message, __LINE__, __FILE__) +#define nassert_raise(message) Notify::assert_failure(message, __LINE__, __FILE__) #endif // NDEBUG diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 89aed0d7ba..5441a5215c 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -2424,6 +2424,7 @@ DTOOL_CONFIG=[ ("PHAVE_DIRENT_H", 'UNDEF', '1'), ("PHAVE_UCONTEXT_H", 'UNDEF', '1'), ("PHAVE_STDINT_H", '1', '1'), + ("PHAVE_EXECINFO_H", 'UNDEF', '1'), ("HAVE_RTTI", '1', '1'), ("HAVE_X11", 'UNDEF', '1'), ("IS_LINUX", 'UNDEF', '1'), @@ -2560,6 +2561,7 @@ def WriteConfigSettings(): dtool_config["PHAVE_GLOB_H"] = 'UNDEF' dtool_config["PHAVE_LOCKF"] = 'UNDEF' dtool_config["HAVE_VIDEO4LINUX"] = 'UNDEF' + dtool_config["PHAVE_EXECINFO_H"] = 'UNDEF' if (GetTarget() == "emscripten"): # There are no threads in JavaScript, so don't bother using them. @@ -2572,6 +2574,7 @@ def WriteConfigSettings(): dtool_config["PHAVE_LINUX_INPUT_H"] = 'UNDEF' dtool_config["HAVE_X11"] = 'UNDEF' dtool_config["HAVE_GLX"] = 'UNDEF' + dtool_config["PHAVE_EXECINFO_H"] = 'UNDEF' # There are no environment vars either, or default prc files. prc_parameters["DEFAULT_PRC_DIR"] = 'UNDEF' From f40e611eed59989e619027d364b2657b5dc6512e Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 14 Oct 2025 15:20:14 +0200 Subject: [PATCH 135/291] parser-inc: Add some missing headers [skip ci] --- dtool/src/parser-inc/emmintrin.h | 1 + dtool/src/parser-inc/random | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 dtool/src/parser-inc/emmintrin.h create mode 100644 dtool/src/parser-inc/random diff --git a/dtool/src/parser-inc/emmintrin.h b/dtool/src/parser-inc/emmintrin.h new file mode 100644 index 0000000000..6589323c8a --- /dev/null +++ b/dtool/src/parser-inc/emmintrin.h @@ -0,0 +1 @@ +#include diff --git a/dtool/src/parser-inc/random b/dtool/src/parser-inc/random new file mode 100644 index 0000000000..e976462950 --- /dev/null +++ b/dtool/src/parser-inc/random @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace std { + class random_device; + class seed_seq; + + template class uniform_int_distribution; + template class uniform_real_distribution; + class bernoulli_distribution; + template class binomial_distribution; + template class geometric_distribution; + template class negative_binomial_distribution; + template class poisson_distribution; + template class exponential_distribution; + template class gamma_distribution; + template class weibull_distribution; + template class extreme_value_distribution; + template class normal_distribution; + template class lognormal_distribution; + template class chi_squared_distribution; + template class cauchy_distribution; + template class fisher_f_distribution; + template class student_t_distribution; + template class discrete_distribution; + template class piecewise_constant_distribution; + template class piecewise_linear_distribution; +} From 5f9bb6550517ac4695db7d04590953d42a804a2e Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 11:42:10 +0200 Subject: [PATCH 136/291] make-prc-key: Update deprecated API use for OpenSSL v3 --- dtool/src/prckeys/makePrcKey.cxx | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/dtool/src/prckeys/makePrcKey.cxx b/dtool/src/prckeys/makePrcKey.cxx index fea188c87c..f6598cab22 100644 --- a/dtool/src/prckeys/makePrcKey.cxx +++ b/dtool/src/prckeys/makePrcKey.cxx @@ -114,6 +114,56 @@ output_c_string(std::ostream &out, const string &string_name, */ EVP_PKEY * generate_key() { +#if OPENSSL_VERSION_MAJOR >= 3 + EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr); + if (ctx == nullptr) { + EVP_PKEY_CTX_free(ctx); + output_ssl_errors(); + exit(1); + } + + if (EVP_PKEY_keygen_init(ctx) <= 0 || + EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 1024) <= 0) { + EVP_PKEY_CTX_free(ctx); + output_ssl_errors(); + exit(1); + } + + BIGNUM *e = BN_new(); + if (e == nullptr || !BN_set_word(e, 7)) { + BN_free(e); + EVP_PKEY_CTX_free(ctx); + output_ssl_errors(); + exit(1); + } + + unsigned char *e_buf = nullptr; + int e_len = BN_num_bytes(e); + e_buf = (unsigned char *)OPENSSL_malloc(e_len); + BN_bn2bin(e, e_buf); + + size_t bits = 1024; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_size_t("bits", &bits), + OSSL_PARAM_construct_BN("e", e_buf, e_len), + OSSL_PARAM_END + }; + + EVP_PKEY *pkey = nullptr; + if (EVP_PKEY_CTX_set_params(ctx, params) <= 0 || + EVP_PKEY_generate(ctx, &pkey) <= 0) { + OPENSSL_free(e_buf); + BN_free(e); + EVP_PKEY_CTX_free(ctx); + output_ssl_errors(); + exit(1); + } + + OPENSSL_free(e_buf); + BN_free(e); + EVP_PKEY_CTX_free(ctx); + return pkey; +#else RSA *rsa = RSA_new(); BIGNUM *e = BN_new(); if (rsa == nullptr || e == nullptr) { @@ -134,6 +184,7 @@ generate_key() { EVP_PKEY *pkey = EVP_PKEY_new(); EVP_PKEY_assign_RSA(pkey, rsa); return pkey; +#endif } /** From 25ef6112de020ae2ad91b2ee012ecfccf2301ad6 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 11:42:38 +0200 Subject: [PATCH 137/291] express: Update SHA256 code in ZipArchive for OpenSSL v3 --- panda/src/express/zipArchive.cxx | 26 ++++++++++++-------------- tests/express/cert.pem | 21 +++++++++++++++++++++ tests/express/private.pem | 28 ++++++++++++++++++++++++++++ tests/express/test_zip.py | 16 ++++++++++++++++ 4 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 tests/express/cert.pem create mode 100644 tests/express/private.pem diff --git a/panda/src/express/zipArchive.cxx b/panda/src/express/zipArchive.cxx index 06bd4f9e28..02e501349f 100644 --- a/panda/src/express/zipArchive.cxx +++ b/panda/src/express/zipArchive.cxx @@ -571,11 +571,11 @@ add_jar_signature(X509 *cert, EVP_PKEY *pkey, const std::string &alias) { const std::string header_digest = "VmrRqAIgAm0FCZViZFzpaP8OfDbN4iY0MyYFuzTMPv8="; std::stringstream manifest; - SHA256_CTX manifest_ctx; - SHA256_Init(&manifest_ctx); + EVP_MD_CTX *manifest_ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(manifest_ctx, EVP_sha256(), nullptr); manifest << header; - SHA256_Update(&manifest_ctx, header.data(), header.size()); + EVP_DigestUpdate(manifest_ctx, header.data(), header.size()); std::ostringstream sigfile_body; @@ -594,20 +594,21 @@ add_jar_signature(X509 *cert, EVP_PKEY *pkey, const std::string &alias) { { std::istream *stream = open_read_subfile(subfile); - SHA256_CTX subfile_ctx; - SHA256_Init(&subfile_ctx); + EVP_MD_CTX *subfile_ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(subfile_ctx, EVP_sha256(), nullptr); char buffer[4096]; stream->read(buffer, sizeof(buffer)); size_t count = stream->gcount(); while (count > 0) { - SHA256_Update(&subfile_ctx, buffer, count); + EVP_DigestUpdate(subfile_ctx, buffer, count); stream->read(buffer, sizeof(buffer)); count = stream->gcount(); } delete stream; - SHA256_Final(digest, &subfile_ctx); + EVP_DigestFinal_ex(subfile_ctx, digest, nullptr); + EVP_MD_CTX_free(subfile_ctx); } // Encode to base64. @@ -616,24 +617,21 @@ add_jar_signature(X509 *cert, EVP_PKEY *pkey, const std::string &alias) { // Encode what we just wrote to the manifest file as well. { unsigned char digest[SHA256_DIGEST_LENGTH]; - - SHA256_CTX section_ctx; - SHA256_Init(§ion_ctx); - SHA256_Update(§ion_ctx, section.data(), section.size()); - SHA256_Final(digest, §ion_ctx); + EVP_Digest(section.data(), section.size(), digest, nullptr, EVP_sha256(), nullptr); sigfile_body << "SHA-256-Digest: " << base64_encode(digest, SHA256_DIGEST_LENGTH) << "\r\n\r\n"; } manifest << section; - SHA256_Update(&manifest_ctx, section.data(), section.size()); + EVP_DigestUpdate(manifest_ctx, section.data(), section.size()); } // The hash for the whole manifest file goes at the beginning of the .SF file. std::stringstream sigfile; { unsigned char digest[SHA256_DIGEST_LENGTH]; - SHA256_Final(digest, &manifest_ctx); + EVP_DigestFinal_ex(manifest_ctx, digest, nullptr); + EVP_MD_CTX_free(manifest_ctx); sigfile << "Signature-Version: 1.0\r\n"; sigfile << "SHA-256-Digest-Manifest-Main-Attributes: " << header_digest << "\r\n"; sigfile << "SHA-256-Digest-Manifest: " << base64_encode(digest, SHA256_DIGEST_LENGTH) << "\r\n\r\n"; diff --git a/tests/express/cert.pem b/tests/express/cert.pem new file mode 100644 index 0000000000..33a3a9f151 --- /dev/null +++ b/tests/express/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIULuBiiqTjDq8eK6pCRMLMTtV5tRUwDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE +CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yNTEwMTUwOTA3MDhaFw0yNjEwMTUw +OTA3MDhaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAa +BgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCjCpKGVRGDaAkdb+Fl7NYv6/d3QEp3HDDN0I8Ns5xBPRlvsnUm +uPcv+VZRtgM8DXFyypqmbknHeayWBwcAdvEor3JsRLYpB+buZl48Sr7rOQMbP3xG +da0XYMA3JNW4MSf7VfNbq9tBcXM+JGHZgyEFkmKPxxdwjJC0g90zOBRlTmmOjI1H +Aus1hTQXeCxkbkTIjggxU5e6ptx4EOpaWotLLIWbuFx9rLTcbuQVx4Pwu+DuRcCp +eXsnWcS/wcGQSUF1586zu6oAFBgv8XPmskzOphK0Ei8eDwyfIHxqF/ej0NAJei1Q +hePOh+fX2eLh6kH4sAc2mFtLbnURxwz3b03tAgMBAAGjUzBRMB0GA1UdDgQWBBT7 +KryYClrgtH/oF518izizsMfX1DAfBgNVHSMEGDAWgBT7KryYClrgtH/oF518iziz +sMfX1DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA+JH1el4P8 +ofA5Us4LBJNS82KJY96dSITKE3W7R6IZj0TZPYgE244eeN/wdbu1QsFrXXS7jYYY +npLlVouiC26aMNIrNoRa0rHqJA7shR4+1iT3hawu8zYH55kooOgZwn8mRe1G7XD+ +TrvFYvgjNi5AgISqkmfuXyzcOfSZ7jt/mv7rVSSs9N1ZVrNOAGDnFN1YLAVXD9eh +cH2i0SrtmFYnK+xYvK/eT1EGngtTdsBwD/GGoaPuuGU9sWVswcR9Gp/FgQBB31ZK +Edeyh2IdydBlHRSzpJkhnUtyJBN29+/P3WW27XUYDyfSma+0X7G8qAWsyBG7qWXi +fIU+/gpvLoNC +-----END CERTIFICATE----- diff --git a/tests/express/private.pem b/tests/express/private.pem new file mode 100644 index 0000000000..3f6759b81c --- /dev/null +++ b/tests/express/private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCjCpKGVRGDaAkd +b+Fl7NYv6/d3QEp3HDDN0I8Ns5xBPRlvsnUmuPcv+VZRtgM8DXFyypqmbknHeayW +BwcAdvEor3JsRLYpB+buZl48Sr7rOQMbP3xGda0XYMA3JNW4MSf7VfNbq9tBcXM+ +JGHZgyEFkmKPxxdwjJC0g90zOBRlTmmOjI1HAus1hTQXeCxkbkTIjggxU5e6ptx4 +EOpaWotLLIWbuFx9rLTcbuQVx4Pwu+DuRcCpeXsnWcS/wcGQSUF1586zu6oAFBgv +8XPmskzOphK0Ei8eDwyfIHxqF/ej0NAJei1QhePOh+fX2eLh6kH4sAc2mFtLbnUR +xwz3b03tAgMBAAECggEAR8txCFxPcPkQAnlw3Mw06TdUapvR7q9oQklTpSnxZbz9 +BXWlJt8OYn6+Zw7qT7hvu6fCAAXS0VcgC5SenbLCsTLJBSoguOK060gCuTQE7FnX +p1kGZZSOGxxMqDu9LPXgcEnB9x0vWJsXr0agHAMlOGnkowF9rd8IHaVvc41/VbhD +kH9mWSlX8jSFZnN3pTJUygpA3/m2fBzs+WWacGukF0c3TNgbXpmf2ODxrqCWsf0e +kv7CW1X64g9T/UPoIXCaaAFwM6AzfykEHvty5Yac2WRavTSSxw619vtNGK6Sw7se +SJo9tykxXa1emhEl+AlHkOz4AqPJ+BXZv3IVBiswtQKBgQDlzPKdNPlIQlEsJXrE +3evmOL8ACqnSQG2kof2ouEBE6Z09e4Wm2VA6SBV4soPiVyOcKZp58ZNiU9MIHVl2 +OKvT1AhsKID2cIYjGYvmr7+IpHqqklAMy3K+EtT6oUGu/Js3wlmxG9KJnCtxGwIH +QjizqaUoAZKwj/j5ZWQPxLYdUwKBgQC1oSlTTVvd+tGtEKXyeG5/xTpQpjyRnKWp +bof2Apj6zaWKx6EWAdmLB7uIk4J1rQqCdEVSbUZ+PGcl1flKoSfSWFOtnb0pdU91 +70WS4hz2ABgMuRlOh1xCk0YaoIpR5My+KsoHQngAqChj5rlcLTS2hJTUcQjItSB3 +tuQf8Ug/vwKBgEaA8bKvzQeRaSwdN6Rs3fJdWKRfoSijocAP/y4jkXxTHG3/lsrR +A59N/GByjKoFyCQiU4W0S16wjx9/ObJewsET3Z2pc5+oeE8OiHC3XRFEPWpVx3+V +b9fHGVbn4KoaUFj0JOaEvbyAkId8HMwzUgg1NTmn+eR315BUvjVkSeeHAoGBALUV +ZNpEul7qfroJayn/oEuLG8TkuGaEwzXVmYBQvqzu8ykWNyh116qhnvtf9iXUzDGc +MrEneazKFBzI5K7fVZCFt4fVSI9ZAkwWrkrvXOh214N32B9PHVDS/IX3oXBcqTS/ +/ISYZIRjI0HW+t9HwtJmBOx5dcSFsUUp9u9R5DKJAoGBAMWEsK5lMz0FFr9g9OLD +lARC8iYtFSgL2klINxeMT3lzhB/7baIoEYbgWNcHWw/qB+li3QGpDn+tlilNHIzU +Y1LTEJFMjoHLanA7qskhkziyVIL5z4VCsguArUHamGajT3ZQ03814rG0OUk7wHfk +9ZN06G0/Ot4cPZEc5Jy6E4pS +-----END PRIVATE KEY----- diff --git a/tests/express/test_zip.py b/tests/express/test_zip.py index 89b869ff69..ee433845d4 100644 --- a/tests/express/test_zip.py +++ b/tests/express/test_zip.py @@ -2,6 +2,7 @@ from panda3d.core import ZipArchive, IStreamWrapper, StringStream, Filename from direct.stdpy.file import StreamIOWrapper import zipfile from io import BytesIO +import os EMPTY_ZIP = b'PK\x05\x06' + b'\x00' * 18 @@ -181,3 +182,18 @@ def test_zip_repack(tmp_path): assert zf.read("test1.txt") == b"contents of first file" assert "test2.txt" not in zf.namelist() assert zf.read("test3.txt") == b"contents of third file" + + +def test_zip_jar_signature(): + cur_dir = Filename.from_os_specific(os.path.dirname(__file__)) + + stream = StringStream() + zip = ZipArchive() + zip.open_read_write(stream) + zip.add_subfile("test.txt", StringStream(b"contents of test file"), 6) + zip.add_jar_signature(Filename(cur_dir, "cert.pem"), Filename(cur_dir, "private.pem")) + zip.close() + + with zipfile.ZipFile(StreamIOWrapper(stream), 'r') as zf: + assert zf.read("META-INF/MANIFEST.MF") == b'Manifest-Version: 1.0\r\n\r\nName: test.txt\r\nSHA-256-Digest: k5XWgStAZvRlNWIcz67qLSzso8Mc+OUG1QOlAwysyhE=\r\n\r\n' + assert zf.read("META-INF/CERT.SF") == b'Signature-Version: 1.0\r\nSHA-256-Digest-Manifest-Main-Attributes: VmrRqAIgAm0FCZViZFzpaP8OfDbN4iY0MyYFuzTMPv8=\r\nSHA-256-Digest-Manifest: 9R83KbhgHCBaYGXhJ/bV2MofgjRU254oUx+YilOvRcE=\r\n\r\nName: test.txt\r\nSHA-256-Digest: q8FmiLsrdoC5XQRaN9KmaPCcd2revsR0NzDul9cK6bk=\r\n\r\n' From b7fb4af197609c21eafa50b35eea3ad8ea0c2410 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 11:58:55 +0200 Subject: [PATCH 138/291] tests: Fix test run with OpenSSL disabled --- tests/express/test_zip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/express/test_zip.py b/tests/express/test_zip.py index ee433845d4..88985db933 100644 --- a/tests/express/test_zip.py +++ b/tests/express/test_zip.py @@ -3,6 +3,7 @@ from direct.stdpy.file import StreamIOWrapper import zipfile from io import BytesIO import os +import pytest EMPTY_ZIP = b'PK\x05\x06' + b'\x00' * 18 @@ -184,6 +185,7 @@ def test_zip_repack(tmp_path): assert zf.read("test3.txt") == b"contents of third file" +@pytest.mark.skipif(not hasattr(ZipArchive, 'add_jar_signature'), reason='OpenSSL support disabled') def test_zip_jar_signature(): cur_dir = Filename.from_os_specific(os.path.dirname(__file__)) From 97b3c1896017e1033ac56bc13cd8ab32b39049a6 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 11:59:07 +0200 Subject: [PATCH 139/291] express: Fix build with older OpenSSL versions --- panda/src/express/zipArchive.cxx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/panda/src/express/zipArchive.cxx b/panda/src/express/zipArchive.cxx index 02e501349f..d34f9c5b41 100644 --- a/panda/src/express/zipArchive.cxx +++ b/panda/src/express/zipArchive.cxx @@ -28,6 +28,11 @@ #include "openSSLWrapper.h" +#if OPENSSL_VERSION_NUMBER < 0x10100000L +#define EVP_MD_CTX_new() EVP_MD_CTX_create() +#define EVP_MD_CTX_free(ctx) EVP_MD_CTX_destroy(ctx) +#endif + using std::streamoff; using std::streampos; using std::streamsize; From e415457c055da354a8d4ecbef4434f8f83b92fb5 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 13:40:30 +0200 Subject: [PATCH 140/291] glstuff: Fix a wrong docstring --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 6865b54a16..ec5ca7ad2c 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -14480,8 +14480,8 @@ upload_texture(CLP(TextureContext) *gtc, bool force, bool uses_mipmaps, } /** - * Loads a texture image, or one page of a cube map image, from system RAM to - * texture memory. + * Loads a texture image, or one view of a multiview texture, from system RAM + * to texture memory. */ bool CLP(GraphicsStateGuardian):: upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload, From 14c4a9ad38cd78c7da3dee23f922b1d35b2eca6b Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 13:47:46 +0200 Subject: [PATCH 141/291] direct: Move particlePath definition to Python (see #1014) --- contrib/src/sceneeditor/seParticlePanel.py | 9 +++++++-- direct/src/showbase/showBase.cxx | 9 --------- direct/src/showbase/showBase.h | 4 ---- direct/src/tkpanels/ParticlePanel.py | 11 +++++++---- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/contrib/src/sceneeditor/seParticlePanel.py b/contrib/src/sceneeditor/seParticlePanel.py index 6ea49cc3b1..3791c62381 100644 --- a/contrib/src/sceneeditor/seParticlePanel.py +++ b/contrib/src/sceneeditor/seParticlePanel.py @@ -1,6 +1,7 @@ """PANDA3D Particle Panel""" # Import Tkinter, Pmw, and the floater code from this directory tree. +from panda3d.core import ConfigVariableSearchPath, Point2, Point3, Vec3, Vec4 from direct.tkwidgets.AppShell import AppShell import os, Pmw from direct.tkwidgets.Dial import AngleDial @@ -17,6 +18,10 @@ from tkinter.filedialog import * from tkinter.simpledialog import askstring +particlePath = ConfigVariableSearchPath("particle-path", + "The directories to search for particle files to be loaded.") + + class ParticlePanel(AppShell): # Override class variables appname = 'Particle Panel' @@ -1041,7 +1046,7 @@ class ParticlePanel(AppShell): def loadParticleEffectFromFile(self): # Find path to particle directory - pPath = getParticlePath() + pPath = particlePath if pPath.getNumDirectories() > 0: if repr(pPath.getDirectory(0)) == '.': path = '.' @@ -1070,7 +1075,7 @@ class ParticlePanel(AppShell): def saveParticleEffectToFile(self): # Find path to particle directory - pPath = getParticlePath() + pPath = particlePath if pPath.getNumDirectories() > 0: if repr(pPath.getDirectory(0)) == '.': path = '.' diff --git a/direct/src/showbase/showBase.cxx b/direct/src/showbase/showBase.cxx index c4aa38b7d5..43a8a16f8e 100644 --- a/direct/src/showbase/showBase.cxx +++ b/direct/src/showbase/showBase.cxx @@ -45,15 +45,6 @@ ConfigureDef(config_showbase); ConfigureFn(config_showbase) { } -ConfigVariableSearchPath particle_path -("particle-path", - PRC_DESC("The directories to search for particle files to be loaded.")); - -ConfigVariableSearchPath & -get_particle_path() { - return particle_path; -} - // Throw the "NewFrame" event in the C++ world. Some of the lerp code depends // on receiving this. void diff --git a/direct/src/showbase/showBase.h b/direct/src/showbase/showBase.h index a578c5159c..25cf5fdf14 100644 --- a/direct/src/showbase/showBase.h +++ b/direct/src/showbase/showBase.h @@ -22,8 +22,6 @@ #include "animControl.h" #include "pointerTo.h" #include "dconfig.h" -#include "dSearchPath.h" -#include "configVariableSearchPath.h" #include "nodePath.h" ConfigureDecl(config_showbase, EXPCL_DIRECT_SHOWBASE, EXPTP_DIRECT_SHOWBASE); @@ -34,8 +32,6 @@ class GraphicsEngine; BEGIN_PUBLISH -EXPCL_DIRECT_SHOWBASE ConfigVariableSearchPath &get_particle_path(); - EXPCL_DIRECT_SHOWBASE void throw_new_frame(); EXPCL_DIRECT_SHOWBASE void init_app_for_gui(); diff --git a/direct/src/tkpanels/ParticlePanel.py b/direct/src/tkpanels/ParticlePanel.py index f64cbad8c9..f17e9f022f 100644 --- a/direct/src/tkpanels/ParticlePanel.py +++ b/direct/src/tkpanels/ParticlePanel.py @@ -3,7 +3,7 @@ __all__ = ['ParticlePanel'] # Import Tkinter, Pmw, and the floater code from this directory tree. -from panda3d.core import ColorBlendAttrib, Filename, Point2, Point3, Vec3, Vec4, getModelPath +from panda3d.core import ColorBlendAttrib, ConfigVariableSearchPath, Filename, Point2, Point3, Vec3, Vec4, getModelPath from panda3d.physics import ( BaseParticleEmitter, BaseParticleRenderer, @@ -36,7 +36,6 @@ from panda3d.physics import ( SpriteParticleRenderer, TangentRingEmitter, ) -from panda3d.direct import getParticlePath from direct.tkwidgets.AppShell import AppShell from direct.tkwidgets import Dial from direct.tkwidgets import Floater @@ -53,6 +52,10 @@ import os import tkinter as tk +particlePath = ConfigVariableSearchPath("particle-path", + "The directories to search for particle files to be loaded.") + + class ParticlePanel(AppShell): # Override class variables appname = 'Particle Panel' @@ -1275,7 +1278,7 @@ class ParticlePanel(AppShell): def loadParticleEffectFromFile(self): # Find path to particle directory - pPath = getParticlePath() + pPath = particlePath if pPath.getNumDirectories() > 0: if repr(pPath.getDirectory(0)) == '.': path = '.' @@ -1303,7 +1306,7 @@ class ParticlePanel(AppShell): def saveParticleEffectToFile(self): # Find path to particle directory - pPath = getParticlePath() + pPath = particlePath if pPath.getNumDirectories() > 0: if repr(pPath.getDirectory(0)) == '.': path = '.' From 93a1da82a1f8da2e0ef65c68b425a4a63117d2f5 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 13:52:02 +0200 Subject: [PATCH 142/291] direct: Remove throw_new_frame from libp3showbase (see #1014) --- direct/src/showbase/ShowBase.py | 13 ++++++------- direct/src/showbase/showBase.cxx | 8 -------- direct/src/showbase/showBase.h | 3 --- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index b80931c597..7bfe5ed5c3 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -62,6 +62,7 @@ from panda3d.core import ( DepthTestAttrib, DepthWriteAttrib, DriveInterface, + EventQueue, ExecutionEnvironment, Filename, FisheyeMaker, @@ -114,7 +115,7 @@ from panda3d.core import ( WindowProperties, getModelPath, ) -from panda3d.direct import throw_new_frame, init_app_for_gui +from panda3d.direct import init_app_for_gui from panda3d.direct import storeAccessibilityShortcutKeys, allowAccessibilityShortcutKeys from . import DConfig @@ -2284,9 +2285,8 @@ class ShowBase(DirectObject.DirectObject): # now until someone complains. time.sleep(0.1) - # Lerp stuff needs this event, and it must be generated in - # C++, not in Python. - throw_new_frame() + # Lerp stuff needs this event, thrown directly on the C++ queue. + EventQueue.getGlobalEventQueue().queueEvent("NewFrame") return Task.cont def __igLoopSync(self, state: object) -> int: @@ -2332,9 +2332,8 @@ class ShowBase(DirectObject.DirectObject): self.cluster.waitForFlipCommand() self.graphicsEngine.flipFrame() - # Lerp stuff needs this event, and it must be generated in - # C++, not in Python. - throw_new_frame() + # Lerp stuff needs this event, thrown directly on the C++ queue. + EventQueue.getGlobalEventQueue().queueEvent("NewFrame") return Task.cont def restart(self, clusterSync: bool = False, cluster=None) -> None: diff --git a/direct/src/showbase/showBase.cxx b/direct/src/showbase/showBase.cxx index 43a8a16f8e..e7ccadaf2d 100644 --- a/direct/src/showbase/showBase.cxx +++ b/direct/src/showbase/showBase.cxx @@ -21,7 +21,6 @@ extern "C" { void CPSEnableForegroundOperation(ProcessSerialNumber* psn); } #include "showBase.h" -#include "throw_event.h" #include "graphicsWindow.h" #include "renderBuffer.h" #include "camera.h" @@ -45,13 +44,6 @@ ConfigureDef(config_showbase); ConfigureFn(config_showbase) { } -// Throw the "NewFrame" event in the C++ world. Some of the lerp code depends -// on receiving this. -void -throw_new_frame() { - throw_event("NewFrame"); -} - // Initialize the application for making a Gui-based app, such as wx. At the // moment, this is a no-op except on Mac. void diff --git a/direct/src/showbase/showBase.h b/direct/src/showbase/showBase.h index 25cf5fdf14..bfea13462a 100644 --- a/direct/src/showbase/showBase.h +++ b/direct/src/showbase/showBase.h @@ -16,7 +16,6 @@ #include "directbase.h" -#include "eventHandler.h" #include "graphicsWindow.h" #include "graphicsPipe.h" #include "animControl.h" @@ -32,8 +31,6 @@ class GraphicsEngine; BEGIN_PUBLISH -EXPCL_DIRECT_SHOWBASE void throw_new_frame(); - EXPCL_DIRECT_SHOWBASE void init_app_for_gui(); // to handle windows stickykeys From 57793822fe194a7b5bd58da1537dfcda7fa49d20 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 20:46:56 +0200 Subject: [PATCH 143/291] glgsg: Fix crash if task chain was destroyed before GSG --- panda/src/glstuff/glGraphicsStateGuardian_src.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index 98bd8d3508..a489599672 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -1267,7 +1267,7 @@ public: pdeque _fences; #ifdef HAVE_THREADS - AsyncTaskChain *_async_chain; + PT(AsyncTaskChain) _async_chain; #endif // Min job system pending a real job system From 983c88e005946b9f26c8b757704ba3000f5f3dcc Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 20:48:56 +0200 Subject: [PATCH 144/291] tests: Make threading tests conditional upon thread support --- tests/stdpy/test_threading.py | 1 + tests/stdpy/test_threading2.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/stdpy/test_threading.py b/tests/stdpy/test_threading.py index 3a4363e810..f5573270bc 100644 --- a/tests/stdpy/test_threading.py +++ b/tests/stdpy/test_threading.py @@ -10,6 +10,7 @@ def test_threading_error(): @pytest.mark.skipif(sys.platform == "emscripten", reason="No threading") +@pytest.mark.skipif(not core.Thread.is_threading_supported(), reason="No threading") def test_threading(): from collections import deque diff --git a/tests/stdpy/test_threading2.py b/tests/stdpy/test_threading2.py index fd7560def9..12da4108ba 100644 --- a/tests/stdpy/test_threading2.py +++ b/tests/stdpy/test_threading2.py @@ -6,6 +6,7 @@ import pytest @pytest.mark.skipif(sys.platform == "emscripten", reason="No threading") +@pytest.mark.skipif(not core.Thread.is_threading_supported(), reason="No threading") def test_threading2(): class BoundedQueue(threading2._Verbose): From 5006f92110d6286855620ce30e7d8fd1829408f4 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 21:21:04 +0200 Subject: [PATCH 145/291] build: Update to interrogate v0.7.1 --- dtool/Config.cmake | 2 +- makepanda/makepandacore.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dtool/Config.cmake b/dtool/Config.cmake index dff731ccd1..35fcd93484 100644 --- a/dtool/Config.cmake +++ b/dtool/Config.cmake @@ -265,7 +265,7 @@ if(BUILD_INTERROGATE) panda3d-interrogate GIT_REPOSITORY https://github.com/panda3d/interrogate.git - GIT_TAG c19b93c00bd31ecdde861f6511e7830d9f69ac91 + GIT_TAG ccbd5f9d79037a9d53d6dfb1a5e9fdf30f2510c1 PREFIX ${_interrogate_dir} CMAKE_ARGS diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 1d79c9061c..ea69688cb4 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -589,8 +589,8 @@ def GetInterrogateDir(): return INTERROGATE_DIR dir = os.path.join(GetOutputDir(), "tmp", "interrogate") - if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.7.0.dist-info")): - oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.7.0" % (sys.executable, dir)) + if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.7.1.dist-info")): + oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.7.1" % (sys.executable, dir)) INTERROGATE_DIR = dir From 867f1747e94506fb89eee02ae3494d49a5d6fe72 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 21:22:03 +0200 Subject: [PATCH 146/291] dtoolbase: Fix out-of-bounds access in pdtoa.cxx --- dtool/src/dtoolbase/pdtoa.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtool/src/dtoolbase/pdtoa.cxx b/dtool/src/dtoolbase/pdtoa.cxx index 552391a6fc..ce47acb339 100644 --- a/dtool/src/dtoolbase/pdtoa.cxx +++ b/dtool/src/dtoolbase/pdtoa.cxx @@ -252,7 +252,7 @@ inline static unsigned CountDecimalDigit32(uint32_t n) { } inline static void DigitGen(const DiyFp& W, const DiyFp& Mp, uint64_t delta, char* buffer, int* len, int* K) { - static const uint32_t kPow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 0, 0, 0, 0, 0 }; + static const uint32_t kPow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 0, 0, 0, 0, 0, 0 }; const DiyFp one(uint64_t(1) << -Mp.e, Mp.e); const DiyFp wp_w = Mp - W; uint32_t p1 = static_cast(Mp.f >> -one.e); From c352e92dbcbda6f5248c1ac066609d68fdcc4152 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 15 Oct 2025 21:22:34 +0200 Subject: [PATCH 147/291] dtoolbase: Fix out-of-bounds access in pdtoa.cxx --- dtool/src/dtoolbase/pdtoa.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtool/src/dtoolbase/pdtoa.cxx b/dtool/src/dtoolbase/pdtoa.cxx index 552391a6fc..ce47acb339 100644 --- a/dtool/src/dtoolbase/pdtoa.cxx +++ b/dtool/src/dtoolbase/pdtoa.cxx @@ -252,7 +252,7 @@ inline static unsigned CountDecimalDigit32(uint32_t n) { } inline static void DigitGen(const DiyFp& W, const DiyFp& Mp, uint64_t delta, char* buffer, int* len, int* K) { - static const uint32_t kPow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 0, 0, 0, 0, 0 }; + static const uint32_t kPow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 0, 0, 0, 0, 0, 0 }; const DiyFp one(uint64_t(1) << -Mp.e, Mp.e); const DiyFp wp_w = Mp - W; uint32_t p1 = static_cast(Mp.f >> -one.e); From c2ab6472155e50309b7e43cae2a7c75dabcb0fa4 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 19 Oct 2025 11:41:08 +0200 Subject: [PATCH 148/291] mac-stats: Widen strip chart sidebar to make smooth button fit on Tahoe --- pandatool/src/mac-stats/macStatsStripChart.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandatool/src/mac-stats/macStatsStripChart.mm b/pandatool/src/mac-stats/macStatsStripChart.mm index 068c41ea37..a43d6f4d73 100644 --- a/pandatool/src/mac-stats/macStatsStripChart.mm +++ b/pandatool/src/mac-stats/macStatsStripChart.mm @@ -23,8 +23,8 @@ static const int default_strip_chart_width = 400; static const int default_strip_chart_height = 200; -static const int minimum_strip_chart_sidebar_width = 116; -static const int default_strip_chart_sidebar_width = 116; +static const int minimum_strip_chart_sidebar_width = 128; +static const int default_strip_chart_sidebar_width = 128; /** * From e83150f382df5d5cebebeb46c54d2d790f8d0b97 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 19 Oct 2025 11:41:51 +0200 Subject: [PATCH 149/291] makepanda: Add more supported macOS SDKs, print used version --- makepanda/makepandacore.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index ea69688cb4..d3ec9e4078 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -2473,7 +2473,7 @@ def SdkLocateMacOSX(archs = []): # Prefer pre-10.14 for now so that we can keep building FMOD. sdk_versions += ["10.13", "10.12"] - sdk_versions += ["14.0", "13.3", "13.1", "13.0", "12.3", "11.3", "11.1", "11.0"] + sdk_versions += ["15.5", "15.4", "15.2", "15.1", "15.0", "14.5", "14.4", "14.2", "14.0", "13.3", "13.1", "13.0", "12.3", "11.3", "11.1", "11.0"] if 'arm64' not in archs: sdk_versions += ["10.15", "10.14"] @@ -2482,16 +2482,20 @@ def SdkLocateMacOSX(archs = []): sdkname = "MacOSX" + version if os.path.exists("/Library/Developer/CommandLineTools/SDKs/%s.sdk" % sdkname): SDK["MACOSX"] = "/Library/Developer/CommandLineTools/SDKs/%s.sdk" % sdkname - return elif os.path.exists("/Developer/SDKs/%s.sdk" % sdkname): SDK["MACOSX"] = "/Developer/SDKs/%s.sdk" % sdkname - return elif os.path.exists("/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/%s.sdk" % sdkname): SDK["MACOSX"] = "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/%s.sdk" % sdkname - return elif xcode_dir and os.path.exists("%s/Platforms/MacOSX.platform/Developer/SDKs/%s.sdk" % (xcode_dir, sdkname)): SDK["MACOSX"] = "%s/Platforms/MacOSX.platform/Developer/SDKs/%s.sdk" % (xcode_dir, sdkname) - return + else: + continue + + if GetVerbose(): + print("Using macOS %s SDK located at %s" % (version, SDK["MACOSX"])) + else: + print("Using macOS %s SDK" % version) + return exit("Couldn't find any suitable MacOSX SDK!") From bd76aba87cb2f77e2be79b2bf67198c483edd8a7 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 19 Oct 2025 14:18:17 +0200 Subject: [PATCH 150/291] audiotraits: fix compile warning on macOS --- panda/src/audiotraits/openalAudioManager.cxx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/panda/src/audiotraits/openalAudioManager.cxx b/panda/src/audiotraits/openalAudioManager.cxx index 5df2d0d880..50f4dcfcbc 100644 --- a/panda/src/audiotraits/openalAudioManager.cxx +++ b/panda/src/audiotraits/openalAudioManager.cxx @@ -156,11 +156,10 @@ OpenALAudioManager() { // We managed to get a device open. alcGetError(_device); // clear errors - ALCboolean is_hrtf_present = alcIsExtensionPresent(_device, "ALC_SOFT_HRTF"); - ALCint attrs[3] = {0}; #ifndef HAVE_OPENAL_FRAMEWORK + ALCboolean is_hrtf_present = alcIsExtensionPresent(_device, "ALC_SOFT_HRTF"); if (is_hrtf_present) { attrs[0] = ALC_HRTF_SOFT; attrs[1] = audio_want_hrtf.get_value() ? ALC_TRUE : ALC_FALSE; From ba717b0db66a1fcda32ae67c68ad6b8f63da4496 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 19 Oct 2025 14:18:55 +0200 Subject: [PATCH 151/291] display: Fix compile warning --- panda/src/display/subprocessWindowBuffer.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/display/subprocessWindowBuffer.cxx b/panda/src/display/subprocessWindowBuffer.cxx index 168e7f0758..39771551d9 100644 --- a/panda/src/display/subprocessWindowBuffer.cxx +++ b/panda/src/display/subprocessWindowBuffer.cxx @@ -110,7 +110,7 @@ new_buffer(int &fd, size_t &mmap_size, string &filename, mmap_size = temp._mmap_size; // Ensure the disk file is large enough. - size_t zero_size = 1024; + const size_t zero_size = 1024; char zero[zero_size]; memset(zero, 0, zero_size); for (size_t bi = 0; bi < mmap_size; bi += zero_size) { From 9df9409eae53aca90b14b3bd5394942f7d95eb16 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 26 Oct 2025 11:10:52 +0100 Subject: [PATCH 152/291] cocoadisplay: Fix some DPI handling issues on resize --- panda/src/cocoadisplay/cocoaGraphicsWindow.mm | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm index 01c7ab8f63..5c4ec7c4f2 100644 --- a/panda/src/cocoadisplay/cocoaGraphicsWindow.mm +++ b/panda/src/cocoadisplay/cocoaGraphicsWindow.mm @@ -774,7 +774,10 @@ set_properties_now(WindowProperties &properties) { if (!_properties.get_fullscreen()) { // We use the view, not the window, to convert the frame size, expressed // in pixels, into points as the "dpi awareness" is managed by the view. - NSSize size = [_view convertSizeFromBacking:NSMakeSize(width, height)]; + NSSize size = NSMakeSize(width, height); + if (dpi_aware) { + size = [_view convertSizeFromBacking:NSMakeSize(width, height)]; + } if (_window != nil) { [_window setContentSize:size]; } @@ -782,7 +785,8 @@ set_properties_now(WindowProperties &properties) { if (cocoadisplay_cat.is_debug()) { cocoadisplay_cat.debug() - << "Setting size to " << width << ", " << height << "\n"; + << "Setting size to " << width << " x " << height + << " (scaled to " << size.width << " x " << size.height << ")\n"; } // Cocoa doesn't send an event, and the other resize-window handlers @@ -1379,7 +1383,11 @@ handle_resize_event() { [_view setFrameSize:contentRect.size]; } - NSRect frame = [_view convertRectToBacking:[_view bounds]]; + NSRect frame = [_view bounds]; + + if (dpi_aware) { + frame = [_view convertRectToBacking:frame]; + } WindowProperties properties; bool changed = false; From ce31e41ab7d612d024ca1dfbdf519176436ea13a Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 26 Oct 2025 12:51:20 +0100 Subject: [PATCH 153/291] dtoolbase: Work around macOS compiler bug with empty vector Observed with macOS 15.4 SDK when passed two equal pointers to vector_uchar --- dtool/src/dtoolbase/pvector.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dtool/src/dtoolbase/pvector.h b/dtool/src/dtoolbase/pvector.h index be60897d21..7561047add 100644 --- a/dtool/src/dtoolbase/pvector.h +++ b/dtool/src/dtoolbase/pvector.h @@ -51,7 +51,13 @@ public: pvector(pvector &&from) noexcept : base_class(std::move(from)) {}; explicit pvector(size_type n, TypeHandle type_handle = pvector_type_handle) : base_class(n, Type(), allocator(type_handle)) { } explicit pvector(size_type n, const Type &value, TypeHandle type_handle = pvector_type_handle) : base_class(n, value, allocator(type_handle)) { } +#ifdef __APPLE__ + pvector(const Type *begin, const Type *end, TypeHandle type_handle = pvector_type_handle) : base_class(allocator(type_handle)) { + this->insert(this->end(), begin, end); + } +#else pvector(const Type *begin, const Type *end, TypeHandle type_handle = pvector_type_handle) : base_class(begin, end, allocator(type_handle)) { } +#endif pvector(std::initializer_list init, TypeHandle type_handle = pvector_type_handle) : base_class(std::move(init), allocator(type_handle)) { } pvector &operator =(const pvector ©) { From fdf67adc17e259ab1ea9de8acc97d0227a26c119 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 26 Oct 2025 12:54:55 +0100 Subject: [PATCH 154/291] dist: Allow passing in custom AndroidManifest.xml --- direct/src/dist/commands.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index cfddcd89af..50a288b471 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -301,6 +301,7 @@ class build_apps(setuptools.Command): self.android_min_sdk_version = 21 self.android_max_sdk_version = None self.android_target_sdk_version = 30 + self.android_manifest_file = None self.gui_apps = {} self.console_apps = {} self.macos_main_app = None @@ -623,8 +624,12 @@ class build_apps(setuptools.Command): self.build_assets(platform, data_dir) - # Generate an AndroidManifest.xml - self.generate_android_manifest(os.path.join(build_dir, 'AndroidManifest.xml')) + # Generate an AndroidManifest.xml if none was provided + manifest_path = os.path.join(build_dir, 'AndroidManifest.xml') + if self.android_manifest_file: + self.copy(self.android_manifest_file, manifest_path) + else: + self.generate_android_manifest(manifest_path) else: self.build_binaries(platform, build_dir, build_dir) self.build_assets(platform, build_dir) From cc5eaa832a2add8690a503d90499ce162a8bc03d Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Oct 2025 22:14:39 +0100 Subject: [PATCH 155/291] Add LambdaTest to BACKERS.md [skip ci] --- BACKERS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BACKERS.md b/BACKERS.md index 2b48940fc3..29838e55e9 100644 --- a/BACKERS.md +++ b/BACKERS.md @@ -5,10 +5,12 @@ This is a list of all the people who are contributing financially to Panda3D. I ## Bronze Sponsors [Route4Me](https://route4me.com/) +[LambdaTest](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor) * [Daniel Stokes](https://opencollective.com/daniel-stokes) * [David Rose](https://opencollective.com/david-rose) * [Route4Me](https://route4me.com/) +* [LambdaTest](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor) ## Benefactors From 53a6d5dc5d146c488985409cdd488ea440918b81 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Oct 2025 22:31:38 +0100 Subject: [PATCH 156/291] readme: Add sponsor logos [skip ci] --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a61d39d87..daa2240b34 100644 --- a/README.md +++ b/README.md @@ -238,9 +238,12 @@ If you would like to support the project financially, visit [our campaign on OpenCollective](https://opencollective.com/panda3d). Your contributions help us accelerate the development of Panda3D. -For the list of backers, see the [BACKERS.md](BACKERS.md) file or visit the -[Sponsors page](https://www.panda3d.org/sponsors) on our web site. Thank you -to everyone who has donated! +For the complete list of backers, see the [BACKERS.md](BACKERS.md) file or +visit the [Sponsors page](https://www.panda3d.org/sponsors) on our web site. +Thank you to everyone who has donated! + +[Route4Me](https://route4me.com/) +[LambdaTest](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor) From f848776d48d8930651efd46b01af597adb6672ad Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 4 Nov 2025 22:04:08 +0100 Subject: [PATCH 157/291] dist: Update Android protobuf files --- direct/src/dist/_proto/Configuration_pb2.py | 803 +---- direct/src/dist/_proto/Resources_pb2.py | 3370 +------------------ direct/src/dist/_proto/config_pb2.py | 1112 +----- direct/src/dist/_proto/files_pb2.py | 323 +- direct/src/dist/_proto/targeting_pb2.py | 1548 +-------- 5 files changed, 398 insertions(+), 6758 deletions(-) diff --git a/direct/src/dist/_proto/Configuration_pb2.py b/direct/src/dist/_proto/Configuration_pb2.py index 54c6d0a80c..b6ed9ba6c3 100644 --- a/direct/src/dist/_proto/Configuration_pb2.py +++ b/direct/src/dist/_proto/Configuration_pb2.py @@ -1,11 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: frameworks/base/tools/aapt2/Configuration.proto +# Protobuf Python Version: 6.33.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'frameworks/base/tools/aapt2/Configuration.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -13,754 +24,44 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor.FileDescriptor( - name='frameworks/base/tools/aapt2/Configuration.proto', - package='aapt.pb', - syntax='proto3', - serialized_options=b'\n\020com.android.aapt', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n/frameworks/base/tools/aapt2/Configuration.proto\x12\x07\x61\x61pt.pb\"\xd9\x14\n\rConfiguration\x12\x0b\n\x03mcc\x18\x01 \x01(\r\x12\x0b\n\x03mnc\x18\x02 \x01(\r\x12\x0e\n\x06locale\x18\x03 \x01(\t\x12@\n\x10layout_direction\x18\x04 \x01(\x0e\x32&.aapt.pb.Configuration.LayoutDirection\x12\x14\n\x0cscreen_width\x18\x05 \x01(\r\x12\x15\n\rscreen_height\x18\x06 \x01(\r\x12\x17\n\x0fscreen_width_dp\x18\x07 \x01(\r\x12\x18\n\x10screen_height_dp\x18\x08 \x01(\r\x12 \n\x18smallest_screen_width_dp\x18\t \x01(\r\x12\x43\n\x12screen_layout_size\x18\n \x01(\x0e\x32\'.aapt.pb.Configuration.ScreenLayoutSize\x12\x43\n\x12screen_layout_long\x18\x0b \x01(\x0e\x32\'.aapt.pb.Configuration.ScreenLayoutLong\x12\x38\n\x0cscreen_round\x18\x0c \x01(\x0e\x32\".aapt.pb.Configuration.ScreenRound\x12?\n\x10wide_color_gamut\x18\r \x01(\x0e\x32%.aapt.pb.Configuration.WideColorGamut\x12\'\n\x03hdr\x18\x0e \x01(\x0e\x32\x1a.aapt.pb.Configuration.Hdr\x12\x37\n\x0borientation\x18\x0f \x01(\x0e\x32\".aapt.pb.Configuration.Orientation\x12\x37\n\x0cui_mode_type\x18\x10 \x01(\x0e\x32!.aapt.pb.Configuration.UiModeType\x12\x39\n\rui_mode_night\x18\x11 \x01(\x0e\x32\".aapt.pb.Configuration.UiModeNight\x12\x0f\n\x07\x64\x65nsity\x18\x12 \x01(\r\x12\x37\n\x0btouchscreen\x18\x13 \x01(\x0e\x32\".aapt.pb.Configuration.Touchscreen\x12\x36\n\x0bkeys_hidden\x18\x14 \x01(\x0e\x32!.aapt.pb.Configuration.KeysHidden\x12\x31\n\x08keyboard\x18\x15 \x01(\x0e\x32\x1f.aapt.pb.Configuration.Keyboard\x12\x34\n\nnav_hidden\x18\x16 \x01(\x0e\x32 .aapt.pb.Configuration.NavHidden\x12\x35\n\nnavigation\x18\x17 \x01(\x0e\x32!.aapt.pb.Configuration.Navigation\x12\x13\n\x0bsdk_version\x18\x18 \x01(\r\x12\x0f\n\x07product\x18\x19 \x01(\t\"a\n\x0fLayoutDirection\x12\x1a\n\x16LAYOUT_DIRECTION_UNSET\x10\x00\x12\x18\n\x14LAYOUT_DIRECTION_LTR\x10\x01\x12\x18\n\x14LAYOUT_DIRECTION_RTL\x10\x02\"\xaa\x01\n\x10ScreenLayoutSize\x12\x1c\n\x18SCREEN_LAYOUT_SIZE_UNSET\x10\x00\x12\x1c\n\x18SCREEN_LAYOUT_SIZE_SMALL\x10\x01\x12\x1d\n\x19SCREEN_LAYOUT_SIZE_NORMAL\x10\x02\x12\x1c\n\x18SCREEN_LAYOUT_SIZE_LARGE\x10\x03\x12\x1d\n\x19SCREEN_LAYOUT_SIZE_XLARGE\x10\x04\"m\n\x10ScreenLayoutLong\x12\x1c\n\x18SCREEN_LAYOUT_LONG_UNSET\x10\x00\x12\x1b\n\x17SCREEN_LAYOUT_LONG_LONG\x10\x01\x12\x1e\n\x1aSCREEN_LAYOUT_LONG_NOTLONG\x10\x02\"X\n\x0bScreenRound\x12\x16\n\x12SCREEN_ROUND_UNSET\x10\x00\x12\x16\n\x12SCREEN_ROUND_ROUND\x10\x01\x12\x19\n\x15SCREEN_ROUND_NOTROUND\x10\x02\"h\n\x0eWideColorGamut\x12\x1a\n\x16WIDE_COLOR_GAMUT_UNSET\x10\x00\x12\x1b\n\x17WIDE_COLOR_GAMUT_WIDECG\x10\x01\x12\x1d\n\x19WIDE_COLOR_GAMUT_NOWIDECG\x10\x02\"3\n\x03Hdr\x12\r\n\tHDR_UNSET\x10\x00\x12\x0e\n\nHDR_HIGHDR\x10\x01\x12\r\n\tHDR_LOWDR\x10\x02\"h\n\x0bOrientation\x12\x15\n\x11ORIENTATION_UNSET\x10\x00\x12\x14\n\x10ORIENTATION_PORT\x10\x01\x12\x14\n\x10ORIENTATION_LAND\x10\x02\x12\x16\n\x12ORIENTATION_SQUARE\x10\x03\"\xd7\x01\n\nUiModeType\x12\x16\n\x12UI_MODE_TYPE_UNSET\x10\x00\x12\x17\n\x13UI_MODE_TYPE_NORMAL\x10\x01\x12\x15\n\x11UI_MODE_TYPE_DESK\x10\x02\x12\x14\n\x10UI_MODE_TYPE_CAR\x10\x03\x12\x1b\n\x17UI_MODE_TYPE_TELEVISION\x10\x04\x12\x1a\n\x16UI_MODE_TYPE_APPLIANCE\x10\x05\x12\x16\n\x12UI_MODE_TYPE_WATCH\x10\x06\x12\x1a\n\x16UI_MODE_TYPE_VRHEADSET\x10\x07\"[\n\x0bUiModeNight\x12\x17\n\x13UI_MODE_NIGHT_UNSET\x10\x00\x12\x17\n\x13UI_MODE_NIGHT_NIGHT\x10\x01\x12\x1a\n\x16UI_MODE_NIGHT_NOTNIGHT\x10\x02\"m\n\x0bTouchscreen\x12\x15\n\x11TOUCHSCREEN_UNSET\x10\x00\x12\x17\n\x13TOUCHSCREEN_NOTOUCH\x10\x01\x12\x16\n\x12TOUCHSCREEN_STYLUS\x10\x02\x12\x16\n\x12TOUCHSCREEN_FINGER\x10\x03\"v\n\nKeysHidden\x12\x15\n\x11KEYS_HIDDEN_UNSET\x10\x00\x12\x1b\n\x17KEYS_HIDDEN_KEYSEXPOSED\x10\x01\x12\x1a\n\x16KEYS_HIDDEN_KEYSHIDDEN\x10\x02\x12\x18\n\x14KEYS_HIDDEN_KEYSSOFT\x10\x03\"`\n\x08Keyboard\x12\x12\n\x0eKEYBOARD_UNSET\x10\x00\x12\x13\n\x0fKEYBOARD_NOKEYS\x10\x01\x12\x13\n\x0fKEYBOARD_QWERTY\x10\x02\x12\x16\n\x12KEYBOARD_TWELVEKEY\x10\x03\"V\n\tNavHidden\x12\x14\n\x10NAV_HIDDEN_UNSET\x10\x00\x12\x19\n\x15NAV_HIDDEN_NAVEXPOSED\x10\x01\x12\x18\n\x14NAV_HIDDEN_NAVHIDDEN\x10\x02\"}\n\nNavigation\x12\x14\n\x10NAVIGATION_UNSET\x10\x00\x12\x14\n\x10NAVIGATION_NONAV\x10\x01\x12\x13\n\x0fNAVIGATION_DPAD\x10\x02\x12\x18\n\x14NAVIGATION_TRACKBALL\x10\x03\x12\x14\n\x10NAVIGATION_WHEEL\x10\x04\x42\x12\n\x10\x63om.android.aaptb\x06proto3' -) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/frameworks/base/tools/aapt2/Configuration.proto\x12\x07\x61\x61pt.pb\"\x97\x16\n\rConfiguration\x12\x0b\n\x03mcc\x18\x01 \x01(\r\x12\x0b\n\x03mnc\x18\x02 \x01(\r\x12\x0e\n\x06locale\x18\x03 \x01(\t\x12@\n\x10layout_direction\x18\x04 \x01(\x0e\x32&.aapt.pb.Configuration.LayoutDirection\x12\x14\n\x0cscreen_width\x18\x05 \x01(\r\x12\x15\n\rscreen_height\x18\x06 \x01(\r\x12\x17\n\x0fscreen_width_dp\x18\x07 \x01(\r\x12\x18\n\x10screen_height_dp\x18\x08 \x01(\r\x12 \n\x18smallest_screen_width_dp\x18\t \x01(\r\x12\x43\n\x12screen_layout_size\x18\n \x01(\x0e\x32\'.aapt.pb.Configuration.ScreenLayoutSize\x12\x43\n\x12screen_layout_long\x18\x0b \x01(\x0e\x32\'.aapt.pb.Configuration.ScreenLayoutLong\x12\x38\n\x0cscreen_round\x18\x0c \x01(\x0e\x32\".aapt.pb.Configuration.ScreenRound\x12?\n\x10wide_color_gamut\x18\r \x01(\x0e\x32%.aapt.pb.Configuration.WideColorGamut\x12\'\n\x03hdr\x18\x0e \x01(\x0e\x32\x1a.aapt.pb.Configuration.Hdr\x12\x37\n\x0borientation\x18\x0f \x01(\x0e\x32\".aapt.pb.Configuration.Orientation\x12\x37\n\x0cui_mode_type\x18\x10 \x01(\x0e\x32!.aapt.pb.Configuration.UiModeType\x12\x39\n\rui_mode_night\x18\x11 \x01(\x0e\x32\".aapt.pb.Configuration.UiModeNight\x12\x0f\n\x07\x64\x65nsity\x18\x12 \x01(\r\x12\x37\n\x0btouchscreen\x18\x13 \x01(\x0e\x32\".aapt.pb.Configuration.Touchscreen\x12\x36\n\x0bkeys_hidden\x18\x14 \x01(\x0e\x32!.aapt.pb.Configuration.KeysHidden\x12\x31\n\x08keyboard\x18\x15 \x01(\x0e\x32\x1f.aapt.pb.Configuration.Keyboard\x12\x34\n\nnav_hidden\x18\x16 \x01(\x0e\x32 .aapt.pb.Configuration.NavHidden\x12\x35\n\nnavigation\x18\x17 \x01(\x0e\x32!.aapt.pb.Configuration.Navigation\x12\x13\n\x0bsdk_version\x18\x18 \x01(\r\x12\x44\n\x12grammatical_gender\x18\x1a \x01(\x0e\x32(.aapt.pb.Configuration.GrammaticalGender\x12\x0f\n\x07product\x18\x19 \x01(\t\"a\n\x0fLayoutDirection\x12\x1a\n\x16LAYOUT_DIRECTION_UNSET\x10\x00\x12\x18\n\x14LAYOUT_DIRECTION_LTR\x10\x01\x12\x18\n\x14LAYOUT_DIRECTION_RTL\x10\x02\"\xaa\x01\n\x10ScreenLayoutSize\x12\x1c\n\x18SCREEN_LAYOUT_SIZE_UNSET\x10\x00\x12\x1c\n\x18SCREEN_LAYOUT_SIZE_SMALL\x10\x01\x12\x1d\n\x19SCREEN_LAYOUT_SIZE_NORMAL\x10\x02\x12\x1c\n\x18SCREEN_LAYOUT_SIZE_LARGE\x10\x03\x12\x1d\n\x19SCREEN_LAYOUT_SIZE_XLARGE\x10\x04\"m\n\x10ScreenLayoutLong\x12\x1c\n\x18SCREEN_LAYOUT_LONG_UNSET\x10\x00\x12\x1b\n\x17SCREEN_LAYOUT_LONG_LONG\x10\x01\x12\x1e\n\x1aSCREEN_LAYOUT_LONG_NOTLONG\x10\x02\"X\n\x0bScreenRound\x12\x16\n\x12SCREEN_ROUND_UNSET\x10\x00\x12\x16\n\x12SCREEN_ROUND_ROUND\x10\x01\x12\x19\n\x15SCREEN_ROUND_NOTROUND\x10\x02\"h\n\x0eWideColorGamut\x12\x1a\n\x16WIDE_COLOR_GAMUT_UNSET\x10\x00\x12\x1b\n\x17WIDE_COLOR_GAMUT_WIDECG\x10\x01\x12\x1d\n\x19WIDE_COLOR_GAMUT_NOWIDECG\x10\x02\"3\n\x03Hdr\x12\r\n\tHDR_UNSET\x10\x00\x12\x0e\n\nHDR_HIGHDR\x10\x01\x12\r\n\tHDR_LOWDR\x10\x02\"h\n\x0bOrientation\x12\x15\n\x11ORIENTATION_UNSET\x10\x00\x12\x14\n\x10ORIENTATION_PORT\x10\x01\x12\x14\n\x10ORIENTATION_LAND\x10\x02\x12\x16\n\x12ORIENTATION_SQUARE\x10\x03\"\xd7\x01\n\nUiModeType\x12\x16\n\x12UI_MODE_TYPE_UNSET\x10\x00\x12\x17\n\x13UI_MODE_TYPE_NORMAL\x10\x01\x12\x15\n\x11UI_MODE_TYPE_DESK\x10\x02\x12\x14\n\x10UI_MODE_TYPE_CAR\x10\x03\x12\x1b\n\x17UI_MODE_TYPE_TELEVISION\x10\x04\x12\x1a\n\x16UI_MODE_TYPE_APPLIANCE\x10\x05\x12\x16\n\x12UI_MODE_TYPE_WATCH\x10\x06\x12\x1a\n\x16UI_MODE_TYPE_VRHEADSET\x10\x07\"[\n\x0bUiModeNight\x12\x17\n\x13UI_MODE_NIGHT_UNSET\x10\x00\x12\x17\n\x13UI_MODE_NIGHT_NIGHT\x10\x01\x12\x1a\n\x16UI_MODE_NIGHT_NOTNIGHT\x10\x02\"m\n\x0bTouchscreen\x12\x15\n\x11TOUCHSCREEN_UNSET\x10\x00\x12\x17\n\x13TOUCHSCREEN_NOTOUCH\x10\x01\x12\x16\n\x12TOUCHSCREEN_STYLUS\x10\x02\x12\x16\n\x12TOUCHSCREEN_FINGER\x10\x03\"v\n\nKeysHidden\x12\x15\n\x11KEYS_HIDDEN_UNSET\x10\x00\x12\x1b\n\x17KEYS_HIDDEN_KEYSEXPOSED\x10\x01\x12\x1a\n\x16KEYS_HIDDEN_KEYSHIDDEN\x10\x02\x12\x18\n\x14KEYS_HIDDEN_KEYSSOFT\x10\x03\"`\n\x08Keyboard\x12\x12\n\x0eKEYBOARD_UNSET\x10\x00\x12\x13\n\x0fKEYBOARD_NOKEYS\x10\x01\x12\x13\n\x0fKEYBOARD_QWERTY\x10\x02\x12\x16\n\x12KEYBOARD_TWELVEKEY\x10\x03\"V\n\tNavHidden\x12\x14\n\x10NAV_HIDDEN_UNSET\x10\x00\x12\x19\n\x15NAV_HIDDEN_NAVEXPOSED\x10\x01\x12\x18\n\x14NAV_HIDDEN_NAVHIDDEN\x10\x02\"}\n\nNavigation\x12\x14\n\x10NAVIGATION_UNSET\x10\x00\x12\x14\n\x10NAVIGATION_NONAV\x10\x01\x12\x13\n\x0fNAVIGATION_DPAD\x10\x02\x12\x18\n\x14NAVIGATION_TRACKBALL\x10\x03\x12\x14\n\x10NAVIGATION_WHEEL\x10\x04\"v\n\x11GrammaticalGender\x12\x14\n\x10GRAM_GENDER_USET\x10\x00\x12\x16\n\x12GRAM_GENDER_NEUTER\x10\x01\x12\x18\n\x14GRAM_GENDER_FEMININE\x10\x02\x12\x19\n\x15GRAM_GENDER_MASCULINE\x10\x03\x42\x12\n\x10\x63om.android.aaptb\x06proto3') - - -_CONFIGURATION_LAYOUTDIRECTION = _descriptor.EnumDescriptor( - name='LayoutDirection', - full_name='aapt.pb.Configuration.LayoutDirection', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='LAYOUT_DIRECTION_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='LAYOUT_DIRECTION_LTR', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='LAYOUT_DIRECTION_RTL', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1119, - serialized_end=1216, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_LAYOUTDIRECTION) - -_CONFIGURATION_SCREENLAYOUTSIZE = _descriptor.EnumDescriptor( - name='ScreenLayoutSize', - full_name='aapt.pb.Configuration.ScreenLayoutSize', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='SCREEN_LAYOUT_SIZE_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SCREEN_LAYOUT_SIZE_SMALL', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SCREEN_LAYOUT_SIZE_NORMAL', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SCREEN_LAYOUT_SIZE_LARGE', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SCREEN_LAYOUT_SIZE_XLARGE', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1219, - serialized_end=1389, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_SCREENLAYOUTSIZE) - -_CONFIGURATION_SCREENLAYOUTLONG = _descriptor.EnumDescriptor( - name='ScreenLayoutLong', - full_name='aapt.pb.Configuration.ScreenLayoutLong', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='SCREEN_LAYOUT_LONG_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SCREEN_LAYOUT_LONG_LONG', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SCREEN_LAYOUT_LONG_NOTLONG', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1391, - serialized_end=1500, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_SCREENLAYOUTLONG) - -_CONFIGURATION_SCREENROUND = _descriptor.EnumDescriptor( - name='ScreenRound', - full_name='aapt.pb.Configuration.ScreenRound', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='SCREEN_ROUND_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SCREEN_ROUND_ROUND', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SCREEN_ROUND_NOTROUND', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1502, - serialized_end=1590, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_SCREENROUND) - -_CONFIGURATION_WIDECOLORGAMUT = _descriptor.EnumDescriptor( - name='WideColorGamut', - full_name='aapt.pb.Configuration.WideColorGamut', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='WIDE_COLOR_GAMUT_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='WIDE_COLOR_GAMUT_WIDECG', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='WIDE_COLOR_GAMUT_NOWIDECG', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1592, - serialized_end=1696, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_WIDECOLORGAMUT) - -_CONFIGURATION_HDR = _descriptor.EnumDescriptor( - name='Hdr', - full_name='aapt.pb.Configuration.Hdr', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='HDR_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='HDR_HIGHDR', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='HDR_LOWDR', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1698, - serialized_end=1749, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_HDR) - -_CONFIGURATION_ORIENTATION = _descriptor.EnumDescriptor( - name='Orientation', - full_name='aapt.pb.Configuration.Orientation', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='ORIENTATION_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ORIENTATION_PORT', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ORIENTATION_LAND', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ORIENTATION_SQUARE', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1751, - serialized_end=1855, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_ORIENTATION) - -_CONFIGURATION_UIMODETYPE = _descriptor.EnumDescriptor( - name='UiModeType', - full_name='aapt.pb.Configuration.UiModeType', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UI_MODE_TYPE_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UI_MODE_TYPE_NORMAL', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UI_MODE_TYPE_DESK', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UI_MODE_TYPE_CAR', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UI_MODE_TYPE_TELEVISION', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UI_MODE_TYPE_APPLIANCE', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UI_MODE_TYPE_WATCH', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UI_MODE_TYPE_VRHEADSET', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1858, - serialized_end=2073, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_UIMODETYPE) - -_CONFIGURATION_UIMODENIGHT = _descriptor.EnumDescriptor( - name='UiModeNight', - full_name='aapt.pb.Configuration.UiModeNight', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UI_MODE_NIGHT_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UI_MODE_NIGHT_NIGHT', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UI_MODE_NIGHT_NOTNIGHT', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2075, - serialized_end=2166, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_UIMODENIGHT) - -_CONFIGURATION_TOUCHSCREEN = _descriptor.EnumDescriptor( - name='Touchscreen', - full_name='aapt.pb.Configuration.Touchscreen', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='TOUCHSCREEN_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TOUCHSCREEN_NOTOUCH', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TOUCHSCREEN_STYLUS', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TOUCHSCREEN_FINGER', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2168, - serialized_end=2277, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_TOUCHSCREEN) - -_CONFIGURATION_KEYSHIDDEN = _descriptor.EnumDescriptor( - name='KeysHidden', - full_name='aapt.pb.Configuration.KeysHidden', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='KEYS_HIDDEN_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='KEYS_HIDDEN_KEYSEXPOSED', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='KEYS_HIDDEN_KEYSHIDDEN', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='KEYS_HIDDEN_KEYSSOFT', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2279, - serialized_end=2397, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_KEYSHIDDEN) - -_CONFIGURATION_KEYBOARD = _descriptor.EnumDescriptor( - name='Keyboard', - full_name='aapt.pb.Configuration.Keyboard', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='KEYBOARD_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='KEYBOARD_NOKEYS', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='KEYBOARD_QWERTY', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='KEYBOARD_TWELVEKEY', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2399, - serialized_end=2495, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_KEYBOARD) - -_CONFIGURATION_NAVHIDDEN = _descriptor.EnumDescriptor( - name='NavHidden', - full_name='aapt.pb.Configuration.NavHidden', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='NAV_HIDDEN_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NAV_HIDDEN_NAVEXPOSED', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NAV_HIDDEN_NAVHIDDEN', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2497, - serialized_end=2583, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_NAVHIDDEN) - -_CONFIGURATION_NAVIGATION = _descriptor.EnumDescriptor( - name='Navigation', - full_name='aapt.pb.Configuration.Navigation', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='NAVIGATION_UNSET', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NAVIGATION_NONAV', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NAVIGATION_DPAD', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NAVIGATION_TRACKBALL', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NAVIGATION_WHEEL', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2585, - serialized_end=2710, -) -_sym_db.RegisterEnumDescriptor(_CONFIGURATION_NAVIGATION) - - -_CONFIGURATION = _descriptor.Descriptor( - name='Configuration', - full_name='aapt.pb.Configuration', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='mcc', full_name='aapt.pb.Configuration.mcc', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='mnc', full_name='aapt.pb.Configuration.mnc', index=1, - number=2, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='locale', full_name='aapt.pb.Configuration.locale', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='layout_direction', full_name='aapt.pb.Configuration.layout_direction', index=3, - number=4, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='screen_width', full_name='aapt.pb.Configuration.screen_width', index=4, - number=5, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='screen_height', full_name='aapt.pb.Configuration.screen_height', index=5, - number=6, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='screen_width_dp', full_name='aapt.pb.Configuration.screen_width_dp', index=6, - number=7, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='screen_height_dp', full_name='aapt.pb.Configuration.screen_height_dp', index=7, - number=8, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='smallest_screen_width_dp', full_name='aapt.pb.Configuration.smallest_screen_width_dp', index=8, - number=9, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='screen_layout_size', full_name='aapt.pb.Configuration.screen_layout_size', index=9, - number=10, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='screen_layout_long', full_name='aapt.pb.Configuration.screen_layout_long', index=10, - number=11, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='screen_round', full_name='aapt.pb.Configuration.screen_round', index=11, - number=12, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='wide_color_gamut', full_name='aapt.pb.Configuration.wide_color_gamut', index=12, - number=13, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='hdr', full_name='aapt.pb.Configuration.hdr', index=13, - number=14, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='orientation', full_name='aapt.pb.Configuration.orientation', index=14, - number=15, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='ui_mode_type', full_name='aapt.pb.Configuration.ui_mode_type', index=15, - number=16, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='ui_mode_night', full_name='aapt.pb.Configuration.ui_mode_night', index=16, - number=17, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='density', full_name='aapt.pb.Configuration.density', index=17, - number=18, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='touchscreen', full_name='aapt.pb.Configuration.touchscreen', index=18, - number=19, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='keys_hidden', full_name='aapt.pb.Configuration.keys_hidden', index=19, - number=20, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='keyboard', full_name='aapt.pb.Configuration.keyboard', index=20, - number=21, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='nav_hidden', full_name='aapt.pb.Configuration.nav_hidden', index=21, - number=22, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='navigation', full_name='aapt.pb.Configuration.navigation', index=22, - number=23, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='sdk_version', full_name='aapt.pb.Configuration.sdk_version', index=23, - number=24, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='product', full_name='aapt.pb.Configuration.product', index=24, - number=25, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _CONFIGURATION_LAYOUTDIRECTION, - _CONFIGURATION_SCREENLAYOUTSIZE, - _CONFIGURATION_SCREENLAYOUTLONG, - _CONFIGURATION_SCREENROUND, - _CONFIGURATION_WIDECOLORGAMUT, - _CONFIGURATION_HDR, - _CONFIGURATION_ORIENTATION, - _CONFIGURATION_UIMODETYPE, - _CONFIGURATION_UIMODENIGHT, - _CONFIGURATION_TOUCHSCREEN, - _CONFIGURATION_KEYSHIDDEN, - _CONFIGURATION_KEYBOARD, - _CONFIGURATION_NAVHIDDEN, - _CONFIGURATION_NAVIGATION, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=61, - serialized_end=2710, -) - -_CONFIGURATION.fields_by_name['layout_direction'].enum_type = _CONFIGURATION_LAYOUTDIRECTION -_CONFIGURATION.fields_by_name['screen_layout_size'].enum_type = _CONFIGURATION_SCREENLAYOUTSIZE -_CONFIGURATION.fields_by_name['screen_layout_long'].enum_type = _CONFIGURATION_SCREENLAYOUTLONG -_CONFIGURATION.fields_by_name['screen_round'].enum_type = _CONFIGURATION_SCREENROUND -_CONFIGURATION.fields_by_name['wide_color_gamut'].enum_type = _CONFIGURATION_WIDECOLORGAMUT -_CONFIGURATION.fields_by_name['hdr'].enum_type = _CONFIGURATION_HDR -_CONFIGURATION.fields_by_name['orientation'].enum_type = _CONFIGURATION_ORIENTATION -_CONFIGURATION.fields_by_name['ui_mode_type'].enum_type = _CONFIGURATION_UIMODETYPE -_CONFIGURATION.fields_by_name['ui_mode_night'].enum_type = _CONFIGURATION_UIMODENIGHT -_CONFIGURATION.fields_by_name['touchscreen'].enum_type = _CONFIGURATION_TOUCHSCREEN -_CONFIGURATION.fields_by_name['keys_hidden'].enum_type = _CONFIGURATION_KEYSHIDDEN -_CONFIGURATION.fields_by_name['keyboard'].enum_type = _CONFIGURATION_KEYBOARD -_CONFIGURATION.fields_by_name['nav_hidden'].enum_type = _CONFIGURATION_NAVHIDDEN -_CONFIGURATION.fields_by_name['navigation'].enum_type = _CONFIGURATION_NAVIGATION -_CONFIGURATION_LAYOUTDIRECTION.containing_type = _CONFIGURATION -_CONFIGURATION_SCREENLAYOUTSIZE.containing_type = _CONFIGURATION -_CONFIGURATION_SCREENLAYOUTLONG.containing_type = _CONFIGURATION -_CONFIGURATION_SCREENROUND.containing_type = _CONFIGURATION -_CONFIGURATION_WIDECOLORGAMUT.containing_type = _CONFIGURATION -_CONFIGURATION_HDR.containing_type = _CONFIGURATION -_CONFIGURATION_ORIENTATION.containing_type = _CONFIGURATION -_CONFIGURATION_UIMODETYPE.containing_type = _CONFIGURATION -_CONFIGURATION_UIMODENIGHT.containing_type = _CONFIGURATION -_CONFIGURATION_TOUCHSCREEN.containing_type = _CONFIGURATION -_CONFIGURATION_KEYSHIDDEN.containing_type = _CONFIGURATION -_CONFIGURATION_KEYBOARD.containing_type = _CONFIGURATION -_CONFIGURATION_NAVHIDDEN.containing_type = _CONFIGURATION -_CONFIGURATION_NAVIGATION.containing_type = _CONFIGURATION -DESCRIPTOR.message_types_by_name['Configuration'] = _CONFIGURATION -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Configuration = _reflection.GeneratedProtocolMessageType('Configuration', (_message.Message,), { - 'DESCRIPTOR' : _CONFIGURATION, - '__module__' : 'frameworks.base.tools.aapt2.Configuration_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Configuration) - }) -_sym_db.RegisterMessage(Configuration) - - -DESCRIPTOR._options = None +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'frameworks.base.tools.aapt2.Configuration_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020com.android.aapt' + _globals['_CONFIGURATION']._serialized_start=61 + _globals['_CONFIGURATION']._serialized_end=2900 + _globals['_CONFIGURATION_LAYOUTDIRECTION']._serialized_start=1189 + _globals['_CONFIGURATION_LAYOUTDIRECTION']._serialized_end=1286 + _globals['_CONFIGURATION_SCREENLAYOUTSIZE']._serialized_start=1289 + _globals['_CONFIGURATION_SCREENLAYOUTSIZE']._serialized_end=1459 + _globals['_CONFIGURATION_SCREENLAYOUTLONG']._serialized_start=1461 + _globals['_CONFIGURATION_SCREENLAYOUTLONG']._serialized_end=1570 + _globals['_CONFIGURATION_SCREENROUND']._serialized_start=1572 + _globals['_CONFIGURATION_SCREENROUND']._serialized_end=1660 + _globals['_CONFIGURATION_WIDECOLORGAMUT']._serialized_start=1662 + _globals['_CONFIGURATION_WIDECOLORGAMUT']._serialized_end=1766 + _globals['_CONFIGURATION_HDR']._serialized_start=1768 + _globals['_CONFIGURATION_HDR']._serialized_end=1819 + _globals['_CONFIGURATION_ORIENTATION']._serialized_start=1821 + _globals['_CONFIGURATION_ORIENTATION']._serialized_end=1925 + _globals['_CONFIGURATION_UIMODETYPE']._serialized_start=1928 + _globals['_CONFIGURATION_UIMODETYPE']._serialized_end=2143 + _globals['_CONFIGURATION_UIMODENIGHT']._serialized_start=2145 + _globals['_CONFIGURATION_UIMODENIGHT']._serialized_end=2236 + _globals['_CONFIGURATION_TOUCHSCREEN']._serialized_start=2238 + _globals['_CONFIGURATION_TOUCHSCREEN']._serialized_end=2347 + _globals['_CONFIGURATION_KEYSHIDDEN']._serialized_start=2349 + _globals['_CONFIGURATION_KEYSHIDDEN']._serialized_end=2467 + _globals['_CONFIGURATION_KEYBOARD']._serialized_start=2469 + _globals['_CONFIGURATION_KEYBOARD']._serialized_end=2565 + _globals['_CONFIGURATION_NAVHIDDEN']._serialized_start=2567 + _globals['_CONFIGURATION_NAVHIDDEN']._serialized_end=2653 + _globals['_CONFIGURATION_NAVIGATION']._serialized_start=2655 + _globals['_CONFIGURATION_NAVIGATION']._serialized_end=2780 + _globals['_CONFIGURATION_GRAMMATICALGENDER']._serialized_start=2782 + _globals['_CONFIGURATION_GRAMMATICALGENDER']._serialized_end=2900 # @@protoc_insertion_point(module_scope) diff --git a/direct/src/dist/_proto/Resources_pb2.py b/direct/src/dist/_proto/Resources_pb2.py index c18aa80284..4056f86068 100644 --- a/direct/src/dist/_proto/Resources_pb2.py +++ b/direct/src/dist/_proto/Resources_pb2.py @@ -1,11 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: frameworks/base/tools/aapt2/Resources.proto +# Protobuf Python Version: 6.33.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'frameworks/base/tools/aapt2/Resources.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -14,3233 +25,130 @@ _sym_db = _symbol_database.Default() from . import Configuration_pb2 as frameworks_dot_base_dot_tools_dot_aapt2_dot_Configuration__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='frameworks/base/tools/aapt2/Resources.proto', - package='aapt.pb', - syntax='proto3', - serialized_options=b'\n\020com.android.aapt', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n+frameworks/base/tools/aapt2/Resources.proto\x12\x07\x61\x61pt.pb\x1a/frameworks/base/tools/aapt2/Configuration.proto\"\x1a\n\nStringPool\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"<\n\x0eSourcePosition\x12\x13\n\x0bline_number\x18\x01 \x01(\r\x12\x15\n\rcolumn_number\x18\x02 \x01(\r\"E\n\x06Source\x12\x10\n\x08path_idx\x18\x01 \x01(\r\x12)\n\x08position\x18\x02 \x01(\x0b\x32\x17.aapt.pb.SourcePosition\"0\n\x0fToolFingerprint\x12\x0c\n\x04tool\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\"\xbb\x01\n\rResourceTable\x12(\n\x0bsource_pool\x18\x01 \x01(\x0b\x32\x13.aapt.pb.StringPool\x12!\n\x07package\x18\x02 \x03(\x0b\x32\x10.aapt.pb.Package\x12)\n\x0boverlayable\x18\x03 \x03(\x0b\x32\x14.aapt.pb.Overlayable\x12\x32\n\x10tool_fingerprint\x18\x04 \x03(\x0b\x32\x18.aapt.pb.ToolFingerprint\"\x17\n\tPackageId\x12\n\n\x02id\x18\x01 \x01(\r\"d\n\x07Package\x12&\n\npackage_id\x18\x01 \x01(\x0b\x32\x12.aapt.pb.PackageId\x12\x14\n\x0cpackage_name\x18\x02 \x01(\t\x12\x1b\n\x04type\x18\x03 \x03(\x0b\x32\r.aapt.pb.Type\"\x14\n\x06TypeId\x12\n\n\x02id\x18\x01 \x01(\r\"U\n\x04Type\x12 \n\x07type_id\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.TypeId\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x1d\n\x05\x65ntry\x18\x03 \x03(\x0b\x32\x0e.aapt.pb.Entry\"\xab\x01\n\nVisibility\x12(\n\x05level\x18\x01 \x01(\x0e\x32\x19.aapt.pb.Visibility.Level\x12\x1f\n\x06source\x18\x02 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x03 \x01(\t\x12\x12\n\nstaged_api\x18\x04 \x01(\x08\"-\n\x05Level\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07PRIVATE\x10\x01\x12\n\n\x06PUBLIC\x10\x02\"<\n\x08\x41llowNew\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\"K\n\x0bOverlayable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1f\n\x06source\x18\x02 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\r\n\x05\x61\x63tor\x18\x03 \x01(\t\"\x95\x02\n\x0fOverlayableItem\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12/\n\x06policy\x18\x03 \x03(\x0e\x32\x1f.aapt.pb.OverlayableItem.Policy\x12\x17\n\x0foverlayable_idx\x18\x04 \x01(\r\"\x85\x01\n\x06Policy\x12\x08\n\x04NONE\x10\x00\x12\n\n\x06PUBLIC\x10\x01\x12\n\n\x06SYSTEM\x10\x02\x12\n\n\x06VENDOR\x10\x03\x12\x0b\n\x07PRODUCT\x10\x04\x12\r\n\tSIGNATURE\x10\x05\x12\x07\n\x03ODM\x10\x06\x12\x07\n\x03OEM\x10\x07\x12\t\n\x05\x41\x43TOR\x10\x08\x12\x14\n\x10\x43ONFIG_SIGNATURE\x10\t\">\n\x08StagedId\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x11\n\tstaged_id\x18\x02 \x01(\r\"\x15\n\x07\x45ntryId\x12\n\n\x02id\x18\x01 \x01(\r\"\x8e\x02\n\x05\x45ntry\x12\"\n\x08\x65ntry_id\x18\x01 \x01(\x0b\x32\x10.aapt.pb.EntryId\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\'\n\nvisibility\x18\x03 \x01(\x0b\x32\x13.aapt.pb.Visibility\x12$\n\tallow_new\x18\x04 \x01(\x0b\x32\x11.aapt.pb.AllowNew\x12\x32\n\x10overlayable_item\x18\x05 \x01(\x0b\x32\x18.aapt.pb.OverlayableItem\x12*\n\x0c\x63onfig_value\x18\x06 \x03(\x0b\x32\x14.aapt.pb.ConfigValue\x12$\n\tstaged_id\x18\x07 \x01(\x0b\x32\x11.aapt.pb.StagedId\"T\n\x0b\x43onfigValue\x12&\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\x16.aapt.pb.Configuration\x12\x1d\n\x05value\x18\x02 \x01(\x0b\x32\x0e.aapt.pb.Value\"\xa1\x01\n\x05Value\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12\x0c\n\x04weak\x18\x03 \x01(\x08\x12\x1d\n\x04item\x18\x04 \x01(\x0b\x32\r.aapt.pb.ItemH\x00\x12\x30\n\x0e\x63ompound_value\x18\x05 \x01(\x0b\x32\x16.aapt.pb.CompoundValueH\x00\x42\x07\n\x05value\"\x8d\x02\n\x04Item\x12!\n\x03ref\x18\x01 \x01(\x0b\x32\x12.aapt.pb.ReferenceH\x00\x12\x1e\n\x03str\x18\x02 \x01(\x0b\x32\x0f.aapt.pb.StringH\x00\x12%\n\x07raw_str\x18\x03 \x01(\x0b\x32\x12.aapt.pb.RawStringH\x00\x12+\n\nstyled_str\x18\x04 \x01(\x0b\x32\x15.aapt.pb.StyledStringH\x00\x12&\n\x04\x66ile\x18\x05 \x01(\x0b\x32\x16.aapt.pb.FileReferenceH\x00\x12\x19\n\x02id\x18\x06 \x01(\x0b\x32\x0b.aapt.pb.IdH\x00\x12\"\n\x04prim\x18\x07 \x01(\x0b\x32\x12.aapt.pb.PrimitiveH\x00\x42\x07\n\x05value\"\xef\x01\n\rCompoundValue\x12\"\n\x04\x61ttr\x18\x01 \x01(\x0b\x32\x12.aapt.pb.AttributeH\x00\x12\x1f\n\x05style\x18\x02 \x01(\x0b\x32\x0e.aapt.pb.StyleH\x00\x12\'\n\tstyleable\x18\x03 \x01(\x0b\x32\x12.aapt.pb.StyleableH\x00\x12\x1f\n\x05\x61rray\x18\x04 \x01(\x0b\x32\x0e.aapt.pb.ArrayH\x00\x12!\n\x06plural\x18\x05 \x01(\x0b\x32\x0f.aapt.pb.PluralH\x00\x12#\n\x05macro\x18\x06 \x01(\x0b\x32\x12.aapt.pb.MacroBodyH\x00\x42\x07\n\x05value\"\x18\n\x07\x42oolean\x12\r\n\x05value\x18\x01 \x01(\x08\"\xd0\x01\n\tReference\x12%\n\x04type\x18\x01 \x01(\x0e\x32\x17.aapt.pb.Reference.Type\x12\n\n\x02id\x18\x02 \x01(\r\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07private\x18\x04 \x01(\x08\x12$\n\nis_dynamic\x18\x05 \x01(\x0b\x32\x10.aapt.pb.Boolean\x12\x12\n\ntype_flags\x18\x06 \x01(\r\x12\x11\n\tallow_raw\x18\x07 \x01(\x08\"$\n\x04Type\x12\r\n\tREFERENCE\x10\x00\x12\r\n\tATTRIBUTE\x10\x01\"\x04\n\x02Id\"\x17\n\x06String\x12\r\n\x05value\x18\x01 \x01(\t\"\x1a\n\tRawString\x12\r\n\x05value\x18\x01 \x01(\t\"\x83\x01\n\x0cStyledString\x12\r\n\x05value\x18\x01 \x01(\t\x12(\n\x04span\x18\x02 \x03(\x0b\x32\x1a.aapt.pb.StyledString.Span\x1a:\n\x04Span\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x12\n\nfirst_char\x18\x02 \x01(\r\x12\x11\n\tlast_char\x18\x03 \x01(\r\"\x85\x01\n\rFileReference\x12\x0c\n\x04path\x18\x01 \x01(\t\x12)\n\x04type\x18\x02 \x01(\x0e\x32\x1b.aapt.pb.FileReference.Type\";\n\x04Type\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x07\n\x03PNG\x10\x01\x12\x0e\n\nBINARY_XML\x10\x02\x12\r\n\tPROTO_XML\x10\x03\"\x83\x04\n\tPrimitive\x12\x31\n\nnull_value\x18\x01 \x01(\x0b\x32\x1b.aapt.pb.Primitive.NullTypeH\x00\x12\x33\n\x0b\x65mpty_value\x18\x02 \x01(\x0b\x32\x1c.aapt.pb.Primitive.EmptyTypeH\x00\x12\x15\n\x0b\x66loat_value\x18\x03 \x01(\x02H\x00\x12\x19\n\x0f\x64imension_value\x18\r \x01(\rH\x00\x12\x18\n\x0e\x66raction_value\x18\x0e \x01(\rH\x00\x12\x1b\n\x11int_decimal_value\x18\x06 \x01(\x05H\x00\x12\x1f\n\x15int_hexadecimal_value\x18\x07 \x01(\rH\x00\x12\x17\n\rboolean_value\x18\x08 \x01(\x08H\x00\x12\x1b\n\x11\x63olor_argb8_value\x18\t \x01(\rH\x00\x12\x1a\n\x10\x63olor_rgb8_value\x18\n \x01(\rH\x00\x12\x1b\n\x11\x63olor_argb4_value\x18\x0b \x01(\rH\x00\x12\x1a\n\x10\x63olor_rgb4_value\x18\x0c \x01(\rH\x00\x12(\n\x1a\x64imension_value_deprecated\x18\x04 \x01(\x02\x42\x02\x18\x01H\x00\x12\'\n\x19\x66raction_value_deprecated\x18\x05 \x01(\x02\x42\x02\x18\x01H\x00\x1a\n\n\x08NullType\x1a\x0b\n\tEmptyTypeB\r\n\x0boneof_value\"\x90\x03\n\tAttribute\x12\x14\n\x0c\x66ormat_flags\x18\x01 \x01(\r\x12\x0f\n\x07min_int\x18\x02 \x01(\x05\x12\x0f\n\x07max_int\x18\x03 \x01(\x05\x12)\n\x06symbol\x18\x04 \x03(\x0b\x32\x19.aapt.pb.Attribute.Symbol\x1ay\n\x06Symbol\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12 \n\x04name\x18\x03 \x01(\x0b\x32\x12.aapt.pb.Reference\x12\r\n\x05value\x18\x04 \x01(\r\x12\x0c\n\x04type\x18\x05 \x01(\r\"\xa4\x01\n\x0b\x46ormatFlags\x12\x08\n\x04NONE\x10\x00\x12\t\n\x03\x41NY\x10\xff\xff\x03\x12\r\n\tREFERENCE\x10\x01\x12\n\n\x06STRING\x10\x02\x12\x0b\n\x07INTEGER\x10\x04\x12\x0b\n\x07\x42OOLEAN\x10\x08\x12\t\n\x05\x43OLOR\x10\x10\x12\t\n\x05\x46LOAT\x10 \x12\r\n\tDIMENSION\x10@\x12\r\n\x08\x46RACTION\x10\x80\x01\x12\n\n\x04\x45NUM\x10\x80\x80\x04\x12\x0b\n\x05\x46LAGS\x10\x80\x80\x08\"\xf1\x01\n\x05Style\x12\"\n\x06parent\x18\x01 \x01(\x0b\x32\x12.aapt.pb.Reference\x12&\n\rparent_source\x18\x02 \x01(\x0b\x32\x0f.aapt.pb.Source\x12#\n\x05\x65ntry\x18\x03 \x03(\x0b\x32\x14.aapt.pb.Style.Entry\x1aw\n\x05\x45ntry\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12\x1f\n\x03key\x18\x03 \x01(\x0b\x32\x12.aapt.pb.Reference\x12\x1b\n\x04item\x18\x04 \x01(\x0b\x32\r.aapt.pb.Item\"\x91\x01\n\tStyleable\x12\'\n\x05\x65ntry\x18\x01 \x03(\x0b\x32\x18.aapt.pb.Styleable.Entry\x1a[\n\x05\x45ntry\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12 \n\x04\x61ttr\x18\x03 \x01(\x0b\x32\x12.aapt.pb.Reference\"\x8a\x01\n\x05\x41rray\x12\'\n\x07\x65lement\x18\x01 \x03(\x0b\x32\x16.aapt.pb.Array.Element\x1aX\n\x07\x45lement\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12\x1b\n\x04item\x18\x03 \x01(\x0b\x32\r.aapt.pb.Item\"\xef\x01\n\x06Plural\x12$\n\x05\x65ntry\x18\x01 \x03(\x0b\x32\x15.aapt.pb.Plural.Entry\x1a|\n\x05\x45ntry\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12$\n\x05\x61rity\x18\x03 \x01(\x0e\x32\x15.aapt.pb.Plural.Arity\x12\x1b\n\x04item\x18\x04 \x01(\x0b\x32\r.aapt.pb.Item\"A\n\x05\x41rity\x12\x08\n\x04ZERO\x10\x00\x12\x07\n\x03ONE\x10\x01\x12\x07\n\x03TWO\x10\x02\x12\x07\n\x03\x46\x45W\x10\x03\x12\x08\n\x04MANY\x10\x04\x12\t\n\x05OTHER\x10\x05\"r\n\x07XmlNode\x12&\n\x07\x65lement\x18\x01 \x01(\x0b\x32\x13.aapt.pb.XmlElementH\x00\x12\x0e\n\x04text\x18\x02 \x01(\tH\x00\x12\'\n\x06source\x18\x03 \x01(\x0b\x32\x17.aapt.pb.SourcePositionB\x06\n\x04node\"\xb2\x01\n\nXmlElement\x12\x34\n\x15namespace_declaration\x18\x01 \x03(\x0b\x32\x15.aapt.pb.XmlNamespace\x12\x15\n\rnamespace_uri\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12(\n\tattribute\x18\x04 \x03(\x0b\x32\x15.aapt.pb.XmlAttribute\x12\x1f\n\x05\x63hild\x18\x05 \x03(\x0b\x32\x10.aapt.pb.XmlNode\"T\n\x0cXmlNamespace\x12\x0e\n\x06prefix\x18\x01 \x01(\t\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\'\n\x06source\x18\x03 \x01(\x0b\x32\x17.aapt.pb.SourcePosition\"\xa6\x01\n\x0cXmlAttribute\x12\x15\n\rnamespace_uri\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\x12\'\n\x06source\x18\x04 \x01(\x0b\x32\x17.aapt.pb.SourcePosition\x12\x13\n\x0bresource_id\x18\x05 \x01(\r\x12$\n\rcompiled_item\x18\x06 \x01(\x0b\x32\r.aapt.pb.Item\"\xe7\x01\n\tMacroBody\x12\x12\n\nraw_string\x18\x01 \x01(\t\x12*\n\x0cstyle_string\x18\x02 \x01(\x0b\x32\x14.aapt.pb.StyleString\x12?\n\x17untranslatable_sections\x18\x03 \x03(\x0b\x32\x1e.aapt.pb.UntranslatableSection\x12\x30\n\x0fnamespace_stack\x18\x04 \x03(\x0b\x32\x17.aapt.pb.NamespaceAlias\x12\'\n\x06source\x18\x05 \x01(\x0b\x32\x17.aapt.pb.SourcePosition\"J\n\x0eNamespaceAlias\x12\x0e\n\x06prefix\x18\x01 \x01(\t\x12\x14\n\x0cpackage_name\x18\x02 \x01(\t\x12\x12\n\nis_private\x18\x03 \x01(\x08\"\x82\x01\n\x0bStyleString\x12\x0b\n\x03str\x18\x01 \x01(\t\x12(\n\x05spans\x18\x02 \x03(\x0b\x32\x19.aapt.pb.StyleString.Span\x1a<\n\x04Span\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bstart_index\x18\x02 \x01(\r\x12\x11\n\tend_index\x18\x03 \x01(\r\"?\n\x15UntranslatableSection\x12\x13\n\x0bstart_index\x18\x01 \x01(\x04\x12\x11\n\tend_index\x18\x02 \x01(\x04\x42\x12\n\x10\x63om.android.aaptb\x06proto3' - , - dependencies=[frameworks_dot_base_dot_tools_dot_aapt2_dot_Configuration__pb2.DESCRIPTOR,]) - - - -_VISIBILITY_LEVEL = _descriptor.EnumDescriptor( - name='Level', - full_name='aapt.pb.Visibility.Level', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNKNOWN', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PRIVATE', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PUBLIC', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=869, - serialized_end=914, -) -_sym_db.RegisterEnumDescriptor(_VISIBILITY_LEVEL) - -_OVERLAYABLEITEM_POLICY = _descriptor.EnumDescriptor( - name='Policy', - full_name='aapt.pb.OverlayableItem.Policy', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='NONE', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PUBLIC', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SYSTEM', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='VENDOR', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PRODUCT', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SIGNATURE', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ODM', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='OEM', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ACTOR', index=8, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='CONFIG_SIGNATURE', index=9, number=9, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1200, - serialized_end=1333, -) -_sym_db.RegisterEnumDescriptor(_OVERLAYABLEITEM_POLICY) - -_REFERENCE_TYPE = _descriptor.EnumDescriptor( - name='Type', - full_name='aapt.pb.Reference.Type', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='REFERENCE', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ATTRIBUTE', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2658, - serialized_end=2694, -) -_sym_db.RegisterEnumDescriptor(_REFERENCE_TYPE) - -_FILEREFERENCE_TYPE = _descriptor.EnumDescriptor( - name='Type', - full_name='aapt.pb.FileReference.Type', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNKNOWN', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PNG', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='BINARY_XML', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PROTO_XML', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2964, - serialized_end=3023, -) -_sym_db.RegisterEnumDescriptor(_FILEREFERENCE_TYPE) - -_ATTRIBUTE_FORMATFLAGS = _descriptor.EnumDescriptor( - name='FormatFlags', - full_name='aapt.pb.Attribute.FormatFlags', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='NONE', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ANY', index=1, number=65535, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='REFERENCE', index=2, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='STRING', index=3, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='INTEGER', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='BOOLEAN', index=5, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='COLOR', index=6, number=16, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='FLOAT', index=7, number=32, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DIMENSION', index=8, number=64, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='FRACTION', index=9, number=128, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ENUM', index=10, number=65536, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='FLAGS', index=11, number=131072, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=3780, - serialized_end=3944, -) -_sym_db.RegisterEnumDescriptor(_ATTRIBUTE_FORMATFLAGS) - -_PLURAL_ARITY = _descriptor.EnumDescriptor( - name='Arity', - full_name='aapt.pb.Plural.Arity', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='ZERO', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ONE', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TWO', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='FEW', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='MANY', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='OTHER', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=4654, - serialized_end=4719, -) -_sym_db.RegisterEnumDescriptor(_PLURAL_ARITY) - - -_STRINGPOOL = _descriptor.Descriptor( - name='StringPool', - full_name='aapt.pb.StringPool', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='data', full_name='aapt.pb.StringPool.data', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=105, - serialized_end=131, -) - - -_SOURCEPOSITION = _descriptor.Descriptor( - name='SourcePosition', - full_name='aapt.pb.SourcePosition', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='line_number', full_name='aapt.pb.SourcePosition.line_number', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='column_number', full_name='aapt.pb.SourcePosition.column_number', index=1, - number=2, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=133, - serialized_end=193, -) - - -_SOURCE = _descriptor.Descriptor( - name='Source', - full_name='aapt.pb.Source', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='path_idx', full_name='aapt.pb.Source.path_idx', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='position', full_name='aapt.pb.Source.position', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=195, - serialized_end=264, -) - - -_TOOLFINGERPRINT = _descriptor.Descriptor( - name='ToolFingerprint', - full_name='aapt.pb.ToolFingerprint', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='tool', full_name='aapt.pb.ToolFingerprint.tool', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='version', full_name='aapt.pb.ToolFingerprint.version', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=266, - serialized_end=314, -) - - -_RESOURCETABLE = _descriptor.Descriptor( - name='ResourceTable', - full_name='aapt.pb.ResourceTable', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source_pool', full_name='aapt.pb.ResourceTable.source_pool', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='package', full_name='aapt.pb.ResourceTable.package', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='overlayable', full_name='aapt.pb.ResourceTable.overlayable', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='tool_fingerprint', full_name='aapt.pb.ResourceTable.tool_fingerprint', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=317, - serialized_end=504, -) - - -_PACKAGEID = _descriptor.Descriptor( - name='PackageId', - full_name='aapt.pb.PackageId', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='id', full_name='aapt.pb.PackageId.id', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=506, - serialized_end=529, -) - - -_PACKAGE = _descriptor.Descriptor( - name='Package', - full_name='aapt.pb.Package', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='package_id', full_name='aapt.pb.Package.package_id', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='package_name', full_name='aapt.pb.Package.package_name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='type', full_name='aapt.pb.Package.type', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=531, - serialized_end=631, -) - - -_TYPEID = _descriptor.Descriptor( - name='TypeId', - full_name='aapt.pb.TypeId', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='id', full_name='aapt.pb.TypeId.id', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=633, - serialized_end=653, -) - - -_TYPE = _descriptor.Descriptor( - name='Type', - full_name='aapt.pb.Type', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='type_id', full_name='aapt.pb.Type.type_id', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='name', full_name='aapt.pb.Type.name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='entry', full_name='aapt.pb.Type.entry', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=655, - serialized_end=740, -) - - -_VISIBILITY = _descriptor.Descriptor( - name='Visibility', - full_name='aapt.pb.Visibility', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='level', full_name='aapt.pb.Visibility.level', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.Visibility.source', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='aapt.pb.Visibility.comment', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='staged_api', full_name='aapt.pb.Visibility.staged_api', index=3, - number=4, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _VISIBILITY_LEVEL, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=743, - serialized_end=914, -) - - -_ALLOWNEW = _descriptor.Descriptor( - name='AllowNew', - full_name='aapt.pb.AllowNew', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.AllowNew.source', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='aapt.pb.AllowNew.comment', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=916, - serialized_end=976, -) - - -_OVERLAYABLE = _descriptor.Descriptor( - name='Overlayable', - full_name='aapt.pb.Overlayable', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='aapt.pb.Overlayable.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.Overlayable.source', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='actor', full_name='aapt.pb.Overlayable.actor', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=978, - serialized_end=1053, -) - - -_OVERLAYABLEITEM = _descriptor.Descriptor( - name='OverlayableItem', - full_name='aapt.pb.OverlayableItem', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.OverlayableItem.source', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='aapt.pb.OverlayableItem.comment', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='policy', full_name='aapt.pb.OverlayableItem.policy', index=2, - number=3, type=14, cpp_type=8, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='overlayable_idx', full_name='aapt.pb.OverlayableItem.overlayable_idx', index=3, - number=4, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _OVERLAYABLEITEM_POLICY, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1056, - serialized_end=1333, -) - - -_STAGEDID = _descriptor.Descriptor( - name='StagedId', - full_name='aapt.pb.StagedId', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.StagedId.source', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='staged_id', full_name='aapt.pb.StagedId.staged_id', index=1, - number=2, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1335, - serialized_end=1397, -) - - -_ENTRYID = _descriptor.Descriptor( - name='EntryId', - full_name='aapt.pb.EntryId', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='id', full_name='aapt.pb.EntryId.id', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1399, - serialized_end=1420, -) - - -_ENTRY = _descriptor.Descriptor( - name='Entry', - full_name='aapt.pb.Entry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='entry_id', full_name='aapt.pb.Entry.entry_id', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='name', full_name='aapt.pb.Entry.name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='visibility', full_name='aapt.pb.Entry.visibility', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='allow_new', full_name='aapt.pb.Entry.allow_new', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='overlayable_item', full_name='aapt.pb.Entry.overlayable_item', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='config_value', full_name='aapt.pb.Entry.config_value', index=5, - number=6, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='staged_id', full_name='aapt.pb.Entry.staged_id', index=6, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1423, - serialized_end=1693, -) - - -_CONFIGVALUE = _descriptor.Descriptor( - name='ConfigValue', - full_name='aapt.pb.ConfigValue', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='config', full_name='aapt.pb.ConfigValue.config', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='value', full_name='aapt.pb.ConfigValue.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1695, - serialized_end=1779, -) - - -_VALUE = _descriptor.Descriptor( - name='Value', - full_name='aapt.pb.Value', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.Value.source', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='aapt.pb.Value.comment', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='weak', full_name='aapt.pb.Value.weak', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='item', full_name='aapt.pb.Value.item', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='compound_value', full_name='aapt.pb.Value.compound_value', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='value', full_name='aapt.pb.Value.value', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=1782, - serialized_end=1943, -) - - -_ITEM = _descriptor.Descriptor( - name='Item', - full_name='aapt.pb.Item', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='ref', full_name='aapt.pb.Item.ref', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='str', full_name='aapt.pb.Item.str', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='raw_str', full_name='aapt.pb.Item.raw_str', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='styled_str', full_name='aapt.pb.Item.styled_str', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='file', full_name='aapt.pb.Item.file', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='id', full_name='aapt.pb.Item.id', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='prim', full_name='aapt.pb.Item.prim', index=6, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='value', full_name='aapt.pb.Item.value', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=1946, - serialized_end=2215, -) - - -_COMPOUNDVALUE = _descriptor.Descriptor( - name='CompoundValue', - full_name='aapt.pb.CompoundValue', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='attr', full_name='aapt.pb.CompoundValue.attr', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='style', full_name='aapt.pb.CompoundValue.style', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='styleable', full_name='aapt.pb.CompoundValue.styleable', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='array', full_name='aapt.pb.CompoundValue.array', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='plural', full_name='aapt.pb.CompoundValue.plural', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='macro', full_name='aapt.pb.CompoundValue.macro', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='value', full_name='aapt.pb.CompoundValue.value', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=2218, - serialized_end=2457, -) - - -_BOOLEAN = _descriptor.Descriptor( - name='Boolean', - full_name='aapt.pb.Boolean', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='aapt.pb.Boolean.value', index=0, - number=1, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2459, - serialized_end=2483, -) - - -_REFERENCE = _descriptor.Descriptor( - name='Reference', - full_name='aapt.pb.Reference', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='type', full_name='aapt.pb.Reference.type', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='id', full_name='aapt.pb.Reference.id', index=1, - number=2, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='name', full_name='aapt.pb.Reference.name', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='private', full_name='aapt.pb.Reference.private', index=3, - number=4, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='is_dynamic', full_name='aapt.pb.Reference.is_dynamic', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='type_flags', full_name='aapt.pb.Reference.type_flags', index=5, - number=6, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='allow_raw', full_name='aapt.pb.Reference.allow_raw', index=6, - number=7, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _REFERENCE_TYPE, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2486, - serialized_end=2694, -) - - -_ID = _descriptor.Descriptor( - name='Id', - full_name='aapt.pb.Id', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2696, - serialized_end=2700, -) - - -_STRING = _descriptor.Descriptor( - name='String', - full_name='aapt.pb.String', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='aapt.pb.String.value', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2702, - serialized_end=2725, -) - - -_RAWSTRING = _descriptor.Descriptor( - name='RawString', - full_name='aapt.pb.RawString', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='aapt.pb.RawString.value', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2727, - serialized_end=2753, -) - - -_STYLEDSTRING_SPAN = _descriptor.Descriptor( - name='Span', - full_name='aapt.pb.StyledString.Span', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='tag', full_name='aapt.pb.StyledString.Span.tag', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='first_char', full_name='aapt.pb.StyledString.Span.first_char', index=1, - number=2, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='last_char', full_name='aapt.pb.StyledString.Span.last_char', index=2, - number=3, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2829, - serialized_end=2887, -) - -_STYLEDSTRING = _descriptor.Descriptor( - name='StyledString', - full_name='aapt.pb.StyledString', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='aapt.pb.StyledString.value', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='span', full_name='aapt.pb.StyledString.span', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_STYLEDSTRING_SPAN, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2756, - serialized_end=2887, -) - - -_FILEREFERENCE = _descriptor.Descriptor( - name='FileReference', - full_name='aapt.pb.FileReference', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='aapt.pb.FileReference.path', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='type', full_name='aapt.pb.FileReference.type', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _FILEREFERENCE_TYPE, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2890, - serialized_end=3023, -) - - -_PRIMITIVE_NULLTYPE = _descriptor.Descriptor( - name='NullType', - full_name='aapt.pb.Primitive.NullType', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3503, - serialized_end=3513, -) - -_PRIMITIVE_EMPTYTYPE = _descriptor.Descriptor( - name='EmptyType', - full_name='aapt.pb.Primitive.EmptyType', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3515, - serialized_end=3526, -) - -_PRIMITIVE = _descriptor.Descriptor( - name='Primitive', - full_name='aapt.pb.Primitive', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='null_value', full_name='aapt.pb.Primitive.null_value', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='empty_value', full_name='aapt.pb.Primitive.empty_value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='float_value', full_name='aapt.pb.Primitive.float_value', index=2, - number=3, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dimension_value', full_name='aapt.pb.Primitive.dimension_value', index=3, - number=13, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='fraction_value', full_name='aapt.pb.Primitive.fraction_value', index=4, - number=14, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='int_decimal_value', full_name='aapt.pb.Primitive.int_decimal_value', index=5, - number=6, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='int_hexadecimal_value', full_name='aapt.pb.Primitive.int_hexadecimal_value', index=6, - number=7, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='boolean_value', full_name='aapt.pb.Primitive.boolean_value', index=7, - number=8, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='color_argb8_value', full_name='aapt.pb.Primitive.color_argb8_value', index=8, - number=9, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='color_rgb8_value', full_name='aapt.pb.Primitive.color_rgb8_value', index=9, - number=10, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='color_argb4_value', full_name='aapt.pb.Primitive.color_argb4_value', index=10, - number=11, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='color_rgb4_value', full_name='aapt.pb.Primitive.color_rgb4_value', index=11, - number=12, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dimension_value_deprecated', full_name='aapt.pb.Primitive.dimension_value_deprecated', index=12, - number=4, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='fraction_value_deprecated', full_name='aapt.pb.Primitive.fraction_value_deprecated', index=13, - number=5, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_PRIMITIVE_NULLTYPE, _PRIMITIVE_EMPTYTYPE, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='oneof_value', full_name='aapt.pb.Primitive.oneof_value', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=3026, - serialized_end=3541, -) - - -_ATTRIBUTE_SYMBOL = _descriptor.Descriptor( - name='Symbol', - full_name='aapt.pb.Attribute.Symbol', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.Attribute.Symbol.source', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='aapt.pb.Attribute.Symbol.comment', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='name', full_name='aapt.pb.Attribute.Symbol.name', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='value', full_name='aapt.pb.Attribute.Symbol.value', index=3, - number=4, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='type', full_name='aapt.pb.Attribute.Symbol.type', index=4, - number=5, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3656, - serialized_end=3777, -) - -_ATTRIBUTE = _descriptor.Descriptor( - name='Attribute', - full_name='aapt.pb.Attribute', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='format_flags', full_name='aapt.pb.Attribute.format_flags', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='min_int', full_name='aapt.pb.Attribute.min_int', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='max_int', full_name='aapt.pb.Attribute.max_int', index=2, - number=3, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='symbol', full_name='aapt.pb.Attribute.symbol', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_ATTRIBUTE_SYMBOL, ], - enum_types=[ - _ATTRIBUTE_FORMATFLAGS, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3544, - serialized_end=3944, -) - - -_STYLE_ENTRY = _descriptor.Descriptor( - name='Entry', - full_name='aapt.pb.Style.Entry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.Style.Entry.source', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='aapt.pb.Style.Entry.comment', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='key', full_name='aapt.pb.Style.Entry.key', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='item', full_name='aapt.pb.Style.Entry.item', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4069, - serialized_end=4188, -) - -_STYLE = _descriptor.Descriptor( - name='Style', - full_name='aapt.pb.Style', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='parent', full_name='aapt.pb.Style.parent', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='parent_source', full_name='aapt.pb.Style.parent_source', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='entry', full_name='aapt.pb.Style.entry', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_STYLE_ENTRY, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3947, - serialized_end=4188, -) - - -_STYLEABLE_ENTRY = _descriptor.Descriptor( - name='Entry', - full_name='aapt.pb.Styleable.Entry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.Styleable.Entry.source', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='aapt.pb.Styleable.Entry.comment', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='attr', full_name='aapt.pb.Styleable.Entry.attr', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4245, - serialized_end=4336, -) - -_STYLEABLE = _descriptor.Descriptor( - name='Styleable', - full_name='aapt.pb.Styleable', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='entry', full_name='aapt.pb.Styleable.entry', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_STYLEABLE_ENTRY, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4191, - serialized_end=4336, -) - - -_ARRAY_ELEMENT = _descriptor.Descriptor( - name='Element', - full_name='aapt.pb.Array.Element', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.Array.Element.source', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='aapt.pb.Array.Element.comment', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='item', full_name='aapt.pb.Array.Element.item', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4389, - serialized_end=4477, -) - -_ARRAY = _descriptor.Descriptor( - name='Array', - full_name='aapt.pb.Array', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='element', full_name='aapt.pb.Array.element', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_ARRAY_ELEMENT, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4339, - serialized_end=4477, -) - - -_PLURAL_ENTRY = _descriptor.Descriptor( - name='Entry', - full_name='aapt.pb.Plural.Entry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.Plural.Entry.source', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='comment', full_name='aapt.pb.Plural.Entry.comment', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='arity', full_name='aapt.pb.Plural.Entry.arity', index=2, - number=3, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='item', full_name='aapt.pb.Plural.Entry.item', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4528, - serialized_end=4652, -) - -_PLURAL = _descriptor.Descriptor( - name='Plural', - full_name='aapt.pb.Plural', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='entry', full_name='aapt.pb.Plural.entry', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_PLURAL_ENTRY, ], - enum_types=[ - _PLURAL_ARITY, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4480, - serialized_end=4719, -) - - -_XMLNODE = _descriptor.Descriptor( - name='XmlNode', - full_name='aapt.pb.XmlNode', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='element', full_name='aapt.pb.XmlNode.element', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='text', full_name='aapt.pb.XmlNode.text', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.XmlNode.source', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='node', full_name='aapt.pb.XmlNode.node', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=4721, - serialized_end=4835, -) - - -_XMLELEMENT = _descriptor.Descriptor( - name='XmlElement', - full_name='aapt.pb.XmlElement', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='namespace_declaration', full_name='aapt.pb.XmlElement.namespace_declaration', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='namespace_uri', full_name='aapt.pb.XmlElement.namespace_uri', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='name', full_name='aapt.pb.XmlElement.name', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='attribute', full_name='aapt.pb.XmlElement.attribute', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='child', full_name='aapt.pb.XmlElement.child', index=4, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=4838, - serialized_end=5016, -) - - -_XMLNAMESPACE = _descriptor.Descriptor( - name='XmlNamespace', - full_name='aapt.pb.XmlNamespace', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='prefix', full_name='aapt.pb.XmlNamespace.prefix', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uri', full_name='aapt.pb.XmlNamespace.uri', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.XmlNamespace.source', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=5018, - serialized_end=5102, -) - - -_XMLATTRIBUTE = _descriptor.Descriptor( - name='XmlAttribute', - full_name='aapt.pb.XmlAttribute', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='namespace_uri', full_name='aapt.pb.XmlAttribute.namespace_uri', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='name', full_name='aapt.pb.XmlAttribute.name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='value', full_name='aapt.pb.XmlAttribute.value', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.XmlAttribute.source', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='resource_id', full_name='aapt.pb.XmlAttribute.resource_id', index=4, - number=5, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='compiled_item', full_name='aapt.pb.XmlAttribute.compiled_item', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=5105, - serialized_end=5271, -) - - -_MACROBODY = _descriptor.Descriptor( - name='MacroBody', - full_name='aapt.pb.MacroBody', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='raw_string', full_name='aapt.pb.MacroBody.raw_string', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='style_string', full_name='aapt.pb.MacroBody.style_string', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='untranslatable_sections', full_name='aapt.pb.MacroBody.untranslatable_sections', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='namespace_stack', full_name='aapt.pb.MacroBody.namespace_stack', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='source', full_name='aapt.pb.MacroBody.source', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=5274, - serialized_end=5505, -) - - -_NAMESPACEALIAS = _descriptor.Descriptor( - name='NamespaceAlias', - full_name='aapt.pb.NamespaceAlias', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='prefix', full_name='aapt.pb.NamespaceAlias.prefix', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='package_name', full_name='aapt.pb.NamespaceAlias.package_name', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='is_private', full_name='aapt.pb.NamespaceAlias.is_private', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=5507, - serialized_end=5581, -) - - -_STYLESTRING_SPAN = _descriptor.Descriptor( - name='Span', - full_name='aapt.pb.StyleString.Span', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='aapt.pb.StyleString.Span.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='start_index', full_name='aapt.pb.StyleString.Span.start_index', index=1, - number=2, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='end_index', full_name='aapt.pb.StyleString.Span.end_index', index=2, - number=3, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=5654, - serialized_end=5714, -) - -_STYLESTRING = _descriptor.Descriptor( - name='StyleString', - full_name='aapt.pb.StyleString', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='str', full_name='aapt.pb.StyleString.str', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='spans', full_name='aapt.pb.StyleString.spans', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[_STYLESTRING_SPAN, ], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=5584, - serialized_end=5714, -) - - -_UNTRANSLATABLESECTION = _descriptor.Descriptor( - name='UntranslatableSection', - full_name='aapt.pb.UntranslatableSection', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='start_index', full_name='aapt.pb.UntranslatableSection.start_index', index=0, - number=1, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='end_index', full_name='aapt.pb.UntranslatableSection.end_index', index=1, - number=2, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=5716, - serialized_end=5779, -) - -_SOURCE.fields_by_name['position'].message_type = _SOURCEPOSITION -_RESOURCETABLE.fields_by_name['source_pool'].message_type = _STRINGPOOL -_RESOURCETABLE.fields_by_name['package'].message_type = _PACKAGE -_RESOURCETABLE.fields_by_name['overlayable'].message_type = _OVERLAYABLE -_RESOURCETABLE.fields_by_name['tool_fingerprint'].message_type = _TOOLFINGERPRINT -_PACKAGE.fields_by_name['package_id'].message_type = _PACKAGEID -_PACKAGE.fields_by_name['type'].message_type = _TYPE -_TYPE.fields_by_name['type_id'].message_type = _TYPEID -_TYPE.fields_by_name['entry'].message_type = _ENTRY -_VISIBILITY.fields_by_name['level'].enum_type = _VISIBILITY_LEVEL -_VISIBILITY.fields_by_name['source'].message_type = _SOURCE -_VISIBILITY_LEVEL.containing_type = _VISIBILITY -_ALLOWNEW.fields_by_name['source'].message_type = _SOURCE -_OVERLAYABLE.fields_by_name['source'].message_type = _SOURCE -_OVERLAYABLEITEM.fields_by_name['source'].message_type = _SOURCE -_OVERLAYABLEITEM.fields_by_name['policy'].enum_type = _OVERLAYABLEITEM_POLICY -_OVERLAYABLEITEM_POLICY.containing_type = _OVERLAYABLEITEM -_STAGEDID.fields_by_name['source'].message_type = _SOURCE -_ENTRY.fields_by_name['entry_id'].message_type = _ENTRYID -_ENTRY.fields_by_name['visibility'].message_type = _VISIBILITY -_ENTRY.fields_by_name['allow_new'].message_type = _ALLOWNEW -_ENTRY.fields_by_name['overlayable_item'].message_type = _OVERLAYABLEITEM -_ENTRY.fields_by_name['config_value'].message_type = _CONFIGVALUE -_ENTRY.fields_by_name['staged_id'].message_type = _STAGEDID -_CONFIGVALUE.fields_by_name['config'].message_type = frameworks_dot_base_dot_tools_dot_aapt2_dot_Configuration__pb2._CONFIGURATION -_CONFIGVALUE.fields_by_name['value'].message_type = _VALUE -_VALUE.fields_by_name['source'].message_type = _SOURCE -_VALUE.fields_by_name['item'].message_type = _ITEM -_VALUE.fields_by_name['compound_value'].message_type = _COMPOUNDVALUE -_VALUE.oneofs_by_name['value'].fields.append( - _VALUE.fields_by_name['item']) -_VALUE.fields_by_name['item'].containing_oneof = _VALUE.oneofs_by_name['value'] -_VALUE.oneofs_by_name['value'].fields.append( - _VALUE.fields_by_name['compound_value']) -_VALUE.fields_by_name['compound_value'].containing_oneof = _VALUE.oneofs_by_name['value'] -_ITEM.fields_by_name['ref'].message_type = _REFERENCE -_ITEM.fields_by_name['str'].message_type = _STRING -_ITEM.fields_by_name['raw_str'].message_type = _RAWSTRING -_ITEM.fields_by_name['styled_str'].message_type = _STYLEDSTRING -_ITEM.fields_by_name['file'].message_type = _FILEREFERENCE -_ITEM.fields_by_name['id'].message_type = _ID -_ITEM.fields_by_name['prim'].message_type = _PRIMITIVE -_ITEM.oneofs_by_name['value'].fields.append( - _ITEM.fields_by_name['ref']) -_ITEM.fields_by_name['ref'].containing_oneof = _ITEM.oneofs_by_name['value'] -_ITEM.oneofs_by_name['value'].fields.append( - _ITEM.fields_by_name['str']) -_ITEM.fields_by_name['str'].containing_oneof = _ITEM.oneofs_by_name['value'] -_ITEM.oneofs_by_name['value'].fields.append( - _ITEM.fields_by_name['raw_str']) -_ITEM.fields_by_name['raw_str'].containing_oneof = _ITEM.oneofs_by_name['value'] -_ITEM.oneofs_by_name['value'].fields.append( - _ITEM.fields_by_name['styled_str']) -_ITEM.fields_by_name['styled_str'].containing_oneof = _ITEM.oneofs_by_name['value'] -_ITEM.oneofs_by_name['value'].fields.append( - _ITEM.fields_by_name['file']) -_ITEM.fields_by_name['file'].containing_oneof = _ITEM.oneofs_by_name['value'] -_ITEM.oneofs_by_name['value'].fields.append( - _ITEM.fields_by_name['id']) -_ITEM.fields_by_name['id'].containing_oneof = _ITEM.oneofs_by_name['value'] -_ITEM.oneofs_by_name['value'].fields.append( - _ITEM.fields_by_name['prim']) -_ITEM.fields_by_name['prim'].containing_oneof = _ITEM.oneofs_by_name['value'] -_COMPOUNDVALUE.fields_by_name['attr'].message_type = _ATTRIBUTE -_COMPOUNDVALUE.fields_by_name['style'].message_type = _STYLE -_COMPOUNDVALUE.fields_by_name['styleable'].message_type = _STYLEABLE -_COMPOUNDVALUE.fields_by_name['array'].message_type = _ARRAY -_COMPOUNDVALUE.fields_by_name['plural'].message_type = _PLURAL -_COMPOUNDVALUE.fields_by_name['macro'].message_type = _MACROBODY -_COMPOUNDVALUE.oneofs_by_name['value'].fields.append( - _COMPOUNDVALUE.fields_by_name['attr']) -_COMPOUNDVALUE.fields_by_name['attr'].containing_oneof = _COMPOUNDVALUE.oneofs_by_name['value'] -_COMPOUNDVALUE.oneofs_by_name['value'].fields.append( - _COMPOUNDVALUE.fields_by_name['style']) -_COMPOUNDVALUE.fields_by_name['style'].containing_oneof = _COMPOUNDVALUE.oneofs_by_name['value'] -_COMPOUNDVALUE.oneofs_by_name['value'].fields.append( - _COMPOUNDVALUE.fields_by_name['styleable']) -_COMPOUNDVALUE.fields_by_name['styleable'].containing_oneof = _COMPOUNDVALUE.oneofs_by_name['value'] -_COMPOUNDVALUE.oneofs_by_name['value'].fields.append( - _COMPOUNDVALUE.fields_by_name['array']) -_COMPOUNDVALUE.fields_by_name['array'].containing_oneof = _COMPOUNDVALUE.oneofs_by_name['value'] -_COMPOUNDVALUE.oneofs_by_name['value'].fields.append( - _COMPOUNDVALUE.fields_by_name['plural']) -_COMPOUNDVALUE.fields_by_name['plural'].containing_oneof = _COMPOUNDVALUE.oneofs_by_name['value'] -_COMPOUNDVALUE.oneofs_by_name['value'].fields.append( - _COMPOUNDVALUE.fields_by_name['macro']) -_COMPOUNDVALUE.fields_by_name['macro'].containing_oneof = _COMPOUNDVALUE.oneofs_by_name['value'] -_REFERENCE.fields_by_name['type'].enum_type = _REFERENCE_TYPE -_REFERENCE.fields_by_name['is_dynamic'].message_type = _BOOLEAN -_REFERENCE_TYPE.containing_type = _REFERENCE -_STYLEDSTRING_SPAN.containing_type = _STYLEDSTRING -_STYLEDSTRING.fields_by_name['span'].message_type = _STYLEDSTRING_SPAN -_FILEREFERENCE.fields_by_name['type'].enum_type = _FILEREFERENCE_TYPE -_FILEREFERENCE_TYPE.containing_type = _FILEREFERENCE -_PRIMITIVE_NULLTYPE.containing_type = _PRIMITIVE -_PRIMITIVE_EMPTYTYPE.containing_type = _PRIMITIVE -_PRIMITIVE.fields_by_name['null_value'].message_type = _PRIMITIVE_NULLTYPE -_PRIMITIVE.fields_by_name['empty_value'].message_type = _PRIMITIVE_EMPTYTYPE -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['null_value']) -_PRIMITIVE.fields_by_name['null_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['empty_value']) -_PRIMITIVE.fields_by_name['empty_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['float_value']) -_PRIMITIVE.fields_by_name['float_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['dimension_value']) -_PRIMITIVE.fields_by_name['dimension_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['fraction_value']) -_PRIMITIVE.fields_by_name['fraction_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['int_decimal_value']) -_PRIMITIVE.fields_by_name['int_decimal_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['int_hexadecimal_value']) -_PRIMITIVE.fields_by_name['int_hexadecimal_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['boolean_value']) -_PRIMITIVE.fields_by_name['boolean_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['color_argb8_value']) -_PRIMITIVE.fields_by_name['color_argb8_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['color_rgb8_value']) -_PRIMITIVE.fields_by_name['color_rgb8_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['color_argb4_value']) -_PRIMITIVE.fields_by_name['color_argb4_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['color_rgb4_value']) -_PRIMITIVE.fields_by_name['color_rgb4_value'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['dimension_value_deprecated']) -_PRIMITIVE.fields_by_name['dimension_value_deprecated'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_PRIMITIVE.oneofs_by_name['oneof_value'].fields.append( - _PRIMITIVE.fields_by_name['fraction_value_deprecated']) -_PRIMITIVE.fields_by_name['fraction_value_deprecated'].containing_oneof = _PRIMITIVE.oneofs_by_name['oneof_value'] -_ATTRIBUTE_SYMBOL.fields_by_name['source'].message_type = _SOURCE -_ATTRIBUTE_SYMBOL.fields_by_name['name'].message_type = _REFERENCE -_ATTRIBUTE_SYMBOL.containing_type = _ATTRIBUTE -_ATTRIBUTE.fields_by_name['symbol'].message_type = _ATTRIBUTE_SYMBOL -_ATTRIBUTE_FORMATFLAGS.containing_type = _ATTRIBUTE -_STYLE_ENTRY.fields_by_name['source'].message_type = _SOURCE -_STYLE_ENTRY.fields_by_name['key'].message_type = _REFERENCE -_STYLE_ENTRY.fields_by_name['item'].message_type = _ITEM -_STYLE_ENTRY.containing_type = _STYLE -_STYLE.fields_by_name['parent'].message_type = _REFERENCE -_STYLE.fields_by_name['parent_source'].message_type = _SOURCE -_STYLE.fields_by_name['entry'].message_type = _STYLE_ENTRY -_STYLEABLE_ENTRY.fields_by_name['source'].message_type = _SOURCE -_STYLEABLE_ENTRY.fields_by_name['attr'].message_type = _REFERENCE -_STYLEABLE_ENTRY.containing_type = _STYLEABLE -_STYLEABLE.fields_by_name['entry'].message_type = _STYLEABLE_ENTRY -_ARRAY_ELEMENT.fields_by_name['source'].message_type = _SOURCE -_ARRAY_ELEMENT.fields_by_name['item'].message_type = _ITEM -_ARRAY_ELEMENT.containing_type = _ARRAY -_ARRAY.fields_by_name['element'].message_type = _ARRAY_ELEMENT -_PLURAL_ENTRY.fields_by_name['source'].message_type = _SOURCE -_PLURAL_ENTRY.fields_by_name['arity'].enum_type = _PLURAL_ARITY -_PLURAL_ENTRY.fields_by_name['item'].message_type = _ITEM -_PLURAL_ENTRY.containing_type = _PLURAL -_PLURAL.fields_by_name['entry'].message_type = _PLURAL_ENTRY -_PLURAL_ARITY.containing_type = _PLURAL -_XMLNODE.fields_by_name['element'].message_type = _XMLELEMENT -_XMLNODE.fields_by_name['source'].message_type = _SOURCEPOSITION -_XMLNODE.oneofs_by_name['node'].fields.append( - _XMLNODE.fields_by_name['element']) -_XMLNODE.fields_by_name['element'].containing_oneof = _XMLNODE.oneofs_by_name['node'] -_XMLNODE.oneofs_by_name['node'].fields.append( - _XMLNODE.fields_by_name['text']) -_XMLNODE.fields_by_name['text'].containing_oneof = _XMLNODE.oneofs_by_name['node'] -_XMLELEMENT.fields_by_name['namespace_declaration'].message_type = _XMLNAMESPACE -_XMLELEMENT.fields_by_name['attribute'].message_type = _XMLATTRIBUTE -_XMLELEMENT.fields_by_name['child'].message_type = _XMLNODE -_XMLNAMESPACE.fields_by_name['source'].message_type = _SOURCEPOSITION -_XMLATTRIBUTE.fields_by_name['source'].message_type = _SOURCEPOSITION -_XMLATTRIBUTE.fields_by_name['compiled_item'].message_type = _ITEM -_MACROBODY.fields_by_name['style_string'].message_type = _STYLESTRING -_MACROBODY.fields_by_name['untranslatable_sections'].message_type = _UNTRANSLATABLESECTION -_MACROBODY.fields_by_name['namespace_stack'].message_type = _NAMESPACEALIAS -_MACROBODY.fields_by_name['source'].message_type = _SOURCEPOSITION -_STYLESTRING_SPAN.containing_type = _STYLESTRING -_STYLESTRING.fields_by_name['spans'].message_type = _STYLESTRING_SPAN -DESCRIPTOR.message_types_by_name['StringPool'] = _STRINGPOOL -DESCRIPTOR.message_types_by_name['SourcePosition'] = _SOURCEPOSITION -DESCRIPTOR.message_types_by_name['Source'] = _SOURCE -DESCRIPTOR.message_types_by_name['ToolFingerprint'] = _TOOLFINGERPRINT -DESCRIPTOR.message_types_by_name['ResourceTable'] = _RESOURCETABLE -DESCRIPTOR.message_types_by_name['PackageId'] = _PACKAGEID -DESCRIPTOR.message_types_by_name['Package'] = _PACKAGE -DESCRIPTOR.message_types_by_name['TypeId'] = _TYPEID -DESCRIPTOR.message_types_by_name['Type'] = _TYPE -DESCRIPTOR.message_types_by_name['Visibility'] = _VISIBILITY -DESCRIPTOR.message_types_by_name['AllowNew'] = _ALLOWNEW -DESCRIPTOR.message_types_by_name['Overlayable'] = _OVERLAYABLE -DESCRIPTOR.message_types_by_name['OverlayableItem'] = _OVERLAYABLEITEM -DESCRIPTOR.message_types_by_name['StagedId'] = _STAGEDID -DESCRIPTOR.message_types_by_name['EntryId'] = _ENTRYID -DESCRIPTOR.message_types_by_name['Entry'] = _ENTRY -DESCRIPTOR.message_types_by_name['ConfigValue'] = _CONFIGVALUE -DESCRIPTOR.message_types_by_name['Value'] = _VALUE -DESCRIPTOR.message_types_by_name['Item'] = _ITEM -DESCRIPTOR.message_types_by_name['CompoundValue'] = _COMPOUNDVALUE -DESCRIPTOR.message_types_by_name['Boolean'] = _BOOLEAN -DESCRIPTOR.message_types_by_name['Reference'] = _REFERENCE -DESCRIPTOR.message_types_by_name['Id'] = _ID -DESCRIPTOR.message_types_by_name['String'] = _STRING -DESCRIPTOR.message_types_by_name['RawString'] = _RAWSTRING -DESCRIPTOR.message_types_by_name['StyledString'] = _STYLEDSTRING -DESCRIPTOR.message_types_by_name['FileReference'] = _FILEREFERENCE -DESCRIPTOR.message_types_by_name['Primitive'] = _PRIMITIVE -DESCRIPTOR.message_types_by_name['Attribute'] = _ATTRIBUTE -DESCRIPTOR.message_types_by_name['Style'] = _STYLE -DESCRIPTOR.message_types_by_name['Styleable'] = _STYLEABLE -DESCRIPTOR.message_types_by_name['Array'] = _ARRAY -DESCRIPTOR.message_types_by_name['Plural'] = _PLURAL -DESCRIPTOR.message_types_by_name['XmlNode'] = _XMLNODE -DESCRIPTOR.message_types_by_name['XmlElement'] = _XMLELEMENT -DESCRIPTOR.message_types_by_name['XmlNamespace'] = _XMLNAMESPACE -DESCRIPTOR.message_types_by_name['XmlAttribute'] = _XMLATTRIBUTE -DESCRIPTOR.message_types_by_name['MacroBody'] = _MACROBODY -DESCRIPTOR.message_types_by_name['NamespaceAlias'] = _NAMESPACEALIAS -DESCRIPTOR.message_types_by_name['StyleString'] = _STYLESTRING -DESCRIPTOR.message_types_by_name['UntranslatableSection'] = _UNTRANSLATABLESECTION -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -StringPool = _reflection.GeneratedProtocolMessageType('StringPool', (_message.Message,), { - 'DESCRIPTOR' : _STRINGPOOL, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.StringPool) - }) -_sym_db.RegisterMessage(StringPool) - -SourcePosition = _reflection.GeneratedProtocolMessageType('SourcePosition', (_message.Message,), { - 'DESCRIPTOR' : _SOURCEPOSITION, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.SourcePosition) - }) -_sym_db.RegisterMessage(SourcePosition) - -Source = _reflection.GeneratedProtocolMessageType('Source', (_message.Message,), { - 'DESCRIPTOR' : _SOURCE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Source) - }) -_sym_db.RegisterMessage(Source) - -ToolFingerprint = _reflection.GeneratedProtocolMessageType('ToolFingerprint', (_message.Message,), { - 'DESCRIPTOR' : _TOOLFINGERPRINT, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.ToolFingerprint) - }) -_sym_db.RegisterMessage(ToolFingerprint) - -ResourceTable = _reflection.GeneratedProtocolMessageType('ResourceTable', (_message.Message,), { - 'DESCRIPTOR' : _RESOURCETABLE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.ResourceTable) - }) -_sym_db.RegisterMessage(ResourceTable) - -PackageId = _reflection.GeneratedProtocolMessageType('PackageId', (_message.Message,), { - 'DESCRIPTOR' : _PACKAGEID, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.PackageId) - }) -_sym_db.RegisterMessage(PackageId) - -Package = _reflection.GeneratedProtocolMessageType('Package', (_message.Message,), { - 'DESCRIPTOR' : _PACKAGE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Package) - }) -_sym_db.RegisterMessage(Package) - -TypeId = _reflection.GeneratedProtocolMessageType('TypeId', (_message.Message,), { - 'DESCRIPTOR' : _TYPEID, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.TypeId) - }) -_sym_db.RegisterMessage(TypeId) - -Type = _reflection.GeneratedProtocolMessageType('Type', (_message.Message,), { - 'DESCRIPTOR' : _TYPE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Type) - }) -_sym_db.RegisterMessage(Type) - -Visibility = _reflection.GeneratedProtocolMessageType('Visibility', (_message.Message,), { - 'DESCRIPTOR' : _VISIBILITY, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Visibility) - }) -_sym_db.RegisterMessage(Visibility) - -AllowNew = _reflection.GeneratedProtocolMessageType('AllowNew', (_message.Message,), { - 'DESCRIPTOR' : _ALLOWNEW, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.AllowNew) - }) -_sym_db.RegisterMessage(AllowNew) - -Overlayable = _reflection.GeneratedProtocolMessageType('Overlayable', (_message.Message,), { - 'DESCRIPTOR' : _OVERLAYABLE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Overlayable) - }) -_sym_db.RegisterMessage(Overlayable) - -OverlayableItem = _reflection.GeneratedProtocolMessageType('OverlayableItem', (_message.Message,), { - 'DESCRIPTOR' : _OVERLAYABLEITEM, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.OverlayableItem) - }) -_sym_db.RegisterMessage(OverlayableItem) - -StagedId = _reflection.GeneratedProtocolMessageType('StagedId', (_message.Message,), { - 'DESCRIPTOR' : _STAGEDID, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.StagedId) - }) -_sym_db.RegisterMessage(StagedId) - -EntryId = _reflection.GeneratedProtocolMessageType('EntryId', (_message.Message,), { - 'DESCRIPTOR' : _ENTRYID, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.EntryId) - }) -_sym_db.RegisterMessage(EntryId) - -Entry = _reflection.GeneratedProtocolMessageType('Entry', (_message.Message,), { - 'DESCRIPTOR' : _ENTRY, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Entry) - }) -_sym_db.RegisterMessage(Entry) - -ConfigValue = _reflection.GeneratedProtocolMessageType('ConfigValue', (_message.Message,), { - 'DESCRIPTOR' : _CONFIGVALUE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.ConfigValue) - }) -_sym_db.RegisterMessage(ConfigValue) - -Value = _reflection.GeneratedProtocolMessageType('Value', (_message.Message,), { - 'DESCRIPTOR' : _VALUE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Value) - }) -_sym_db.RegisterMessage(Value) - -Item = _reflection.GeneratedProtocolMessageType('Item', (_message.Message,), { - 'DESCRIPTOR' : _ITEM, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Item) - }) -_sym_db.RegisterMessage(Item) - -CompoundValue = _reflection.GeneratedProtocolMessageType('CompoundValue', (_message.Message,), { - 'DESCRIPTOR' : _COMPOUNDVALUE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.CompoundValue) - }) -_sym_db.RegisterMessage(CompoundValue) - -Boolean = _reflection.GeneratedProtocolMessageType('Boolean', (_message.Message,), { - 'DESCRIPTOR' : _BOOLEAN, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Boolean) - }) -_sym_db.RegisterMessage(Boolean) - -Reference = _reflection.GeneratedProtocolMessageType('Reference', (_message.Message,), { - 'DESCRIPTOR' : _REFERENCE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Reference) - }) -_sym_db.RegisterMessage(Reference) - -Id = _reflection.GeneratedProtocolMessageType('Id', (_message.Message,), { - 'DESCRIPTOR' : _ID, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Id) - }) -_sym_db.RegisterMessage(Id) - -String = _reflection.GeneratedProtocolMessageType('String', (_message.Message,), { - 'DESCRIPTOR' : _STRING, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.String) - }) -_sym_db.RegisterMessage(String) - -RawString = _reflection.GeneratedProtocolMessageType('RawString', (_message.Message,), { - 'DESCRIPTOR' : _RAWSTRING, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.RawString) - }) -_sym_db.RegisterMessage(RawString) - -StyledString = _reflection.GeneratedProtocolMessageType('StyledString', (_message.Message,), { - - 'Span' : _reflection.GeneratedProtocolMessageType('Span', (_message.Message,), { - 'DESCRIPTOR' : _STYLEDSTRING_SPAN, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.StyledString.Span) - }) - , - 'DESCRIPTOR' : _STYLEDSTRING, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.StyledString) - }) -_sym_db.RegisterMessage(StyledString) -_sym_db.RegisterMessage(StyledString.Span) - -FileReference = _reflection.GeneratedProtocolMessageType('FileReference', (_message.Message,), { - 'DESCRIPTOR' : _FILEREFERENCE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.FileReference) - }) -_sym_db.RegisterMessage(FileReference) - -Primitive = _reflection.GeneratedProtocolMessageType('Primitive', (_message.Message,), { - - 'NullType' : _reflection.GeneratedProtocolMessageType('NullType', (_message.Message,), { - 'DESCRIPTOR' : _PRIMITIVE_NULLTYPE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Primitive.NullType) - }) - , - - 'EmptyType' : _reflection.GeneratedProtocolMessageType('EmptyType', (_message.Message,), { - 'DESCRIPTOR' : _PRIMITIVE_EMPTYTYPE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Primitive.EmptyType) - }) - , - 'DESCRIPTOR' : _PRIMITIVE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Primitive) - }) -_sym_db.RegisterMessage(Primitive) -_sym_db.RegisterMessage(Primitive.NullType) -_sym_db.RegisterMessage(Primitive.EmptyType) - -Attribute = _reflection.GeneratedProtocolMessageType('Attribute', (_message.Message,), { - - 'Symbol' : _reflection.GeneratedProtocolMessageType('Symbol', (_message.Message,), { - 'DESCRIPTOR' : _ATTRIBUTE_SYMBOL, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Attribute.Symbol) - }) - , - 'DESCRIPTOR' : _ATTRIBUTE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Attribute) - }) -_sym_db.RegisterMessage(Attribute) -_sym_db.RegisterMessage(Attribute.Symbol) - -Style = _reflection.GeneratedProtocolMessageType('Style', (_message.Message,), { - - 'Entry' : _reflection.GeneratedProtocolMessageType('Entry', (_message.Message,), { - 'DESCRIPTOR' : _STYLE_ENTRY, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Style.Entry) - }) - , - 'DESCRIPTOR' : _STYLE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Style) - }) -_sym_db.RegisterMessage(Style) -_sym_db.RegisterMessage(Style.Entry) - -Styleable = _reflection.GeneratedProtocolMessageType('Styleable', (_message.Message,), { - - 'Entry' : _reflection.GeneratedProtocolMessageType('Entry', (_message.Message,), { - 'DESCRIPTOR' : _STYLEABLE_ENTRY, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Styleable.Entry) - }) - , - 'DESCRIPTOR' : _STYLEABLE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Styleable) - }) -_sym_db.RegisterMessage(Styleable) -_sym_db.RegisterMessage(Styleable.Entry) - -Array = _reflection.GeneratedProtocolMessageType('Array', (_message.Message,), { - - 'Element' : _reflection.GeneratedProtocolMessageType('Element', (_message.Message,), { - 'DESCRIPTOR' : _ARRAY_ELEMENT, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Array.Element) - }) - , - 'DESCRIPTOR' : _ARRAY, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Array) - }) -_sym_db.RegisterMessage(Array) -_sym_db.RegisterMessage(Array.Element) - -Plural = _reflection.GeneratedProtocolMessageType('Plural', (_message.Message,), { - - 'Entry' : _reflection.GeneratedProtocolMessageType('Entry', (_message.Message,), { - 'DESCRIPTOR' : _PLURAL_ENTRY, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Plural.Entry) - }) - , - 'DESCRIPTOR' : _PLURAL, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.Plural) - }) -_sym_db.RegisterMessage(Plural) -_sym_db.RegisterMessage(Plural.Entry) - -XmlNode = _reflection.GeneratedProtocolMessageType('XmlNode', (_message.Message,), { - 'DESCRIPTOR' : _XMLNODE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.XmlNode) - }) -_sym_db.RegisterMessage(XmlNode) - -XmlElement = _reflection.GeneratedProtocolMessageType('XmlElement', (_message.Message,), { - 'DESCRIPTOR' : _XMLELEMENT, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.XmlElement) - }) -_sym_db.RegisterMessage(XmlElement) - -XmlNamespace = _reflection.GeneratedProtocolMessageType('XmlNamespace', (_message.Message,), { - 'DESCRIPTOR' : _XMLNAMESPACE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.XmlNamespace) - }) -_sym_db.RegisterMessage(XmlNamespace) - -XmlAttribute = _reflection.GeneratedProtocolMessageType('XmlAttribute', (_message.Message,), { - 'DESCRIPTOR' : _XMLATTRIBUTE, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.XmlAttribute) - }) -_sym_db.RegisterMessage(XmlAttribute) - -MacroBody = _reflection.GeneratedProtocolMessageType('MacroBody', (_message.Message,), { - 'DESCRIPTOR' : _MACROBODY, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.MacroBody) - }) -_sym_db.RegisterMessage(MacroBody) - -NamespaceAlias = _reflection.GeneratedProtocolMessageType('NamespaceAlias', (_message.Message,), { - 'DESCRIPTOR' : _NAMESPACEALIAS, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.NamespaceAlias) - }) -_sym_db.RegisterMessage(NamespaceAlias) - -StyleString = _reflection.GeneratedProtocolMessageType('StyleString', (_message.Message,), { - - 'Span' : _reflection.GeneratedProtocolMessageType('Span', (_message.Message,), { - 'DESCRIPTOR' : _STYLESTRING_SPAN, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.StyleString.Span) - }) - , - 'DESCRIPTOR' : _STYLESTRING, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.StyleString) - }) -_sym_db.RegisterMessage(StyleString) -_sym_db.RegisterMessage(StyleString.Span) - -UntranslatableSection = _reflection.GeneratedProtocolMessageType('UntranslatableSection', (_message.Message,), { - 'DESCRIPTOR' : _UNTRANSLATABLESECTION, - '__module__' : 'frameworks.base.tools.aapt2.Resources_pb2' - # @@protoc_insertion_point(class_scope:aapt.pb.UntranslatableSection) - }) -_sym_db.RegisterMessage(UntranslatableSection) - - -DESCRIPTOR._options = None -_PRIMITIVE.fields_by_name['dimension_value_deprecated']._options = None -_PRIMITIVE.fields_by_name['fraction_value_deprecated']._options = None +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+frameworks/base/tools/aapt2/Resources.proto\x12\x07\x61\x61pt.pb\x1a/frameworks/base/tools/aapt2/Configuration.proto\"\x1a\n\nStringPool\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"<\n\x0eSourcePosition\x12\x13\n\x0bline_number\x18\x01 \x01(\r\x12\x15\n\rcolumn_number\x18\x02 \x01(\r\"E\n\x06Source\x12\x10\n\x08path_idx\x18\x01 \x01(\r\x12)\n\x08position\x18\x02 \x01(\x0b\x32\x17.aapt.pb.SourcePosition\"0\n\x0fToolFingerprint\x12\x0c\n\x04tool\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\"O\n\x0f\x44ynamicRefTable\x12&\n\npackage_id\x18\x01 \x01(\x0b\x32\x12.aapt.pb.PackageId\x12\x14\n\x0cpackage_name\x18\x02 \x01(\t\"\xf0\x01\n\rResourceTable\x12(\n\x0bsource_pool\x18\x01 \x01(\x0b\x32\x13.aapt.pb.StringPool\x12!\n\x07package\x18\x02 \x03(\x0b\x32\x10.aapt.pb.Package\x12)\n\x0boverlayable\x18\x03 \x03(\x0b\x32\x14.aapt.pb.Overlayable\x12\x32\n\x10tool_fingerprint\x18\x04 \x03(\x0b\x32\x18.aapt.pb.ToolFingerprint\x12\x33\n\x11\x64ynamic_ref_table\x18\x05 \x03(\x0b\x32\x18.aapt.pb.DynamicRefTable\"\x17\n\tPackageId\x12\n\n\x02id\x18\x01 \x01(\r\"d\n\x07Package\x12&\n\npackage_id\x18\x01 \x01(\x0b\x32\x12.aapt.pb.PackageId\x12\x14\n\x0cpackage_name\x18\x02 \x01(\t\x12\x1b\n\x04type\x18\x03 \x03(\x0b\x32\r.aapt.pb.Type\"\x14\n\x06TypeId\x12\n\n\x02id\x18\x01 \x01(\r\"U\n\x04Type\x12 \n\x07type_id\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.TypeId\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x1d\n\x05\x65ntry\x18\x03 \x03(\x0b\x32\x0e.aapt.pb.Entry\"\xab\x01\n\nVisibility\x12(\n\x05level\x18\x01 \x01(\x0e\x32\x19.aapt.pb.Visibility.Level\x12\x1f\n\x06source\x18\x02 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x03 \x01(\t\x12\x12\n\nstaged_api\x18\x04 \x01(\x08\"-\n\x05Level\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07PRIVATE\x10\x01\x12\n\n\x06PUBLIC\x10\x02\"<\n\x08\x41llowNew\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\"K\n\x0bOverlayable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1f\n\x06source\x18\x02 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\r\n\x05\x61\x63tor\x18\x03 \x01(\t\"\x95\x02\n\x0fOverlayableItem\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12/\n\x06policy\x18\x03 \x03(\x0e\x32\x1f.aapt.pb.OverlayableItem.Policy\x12\x17\n\x0foverlayable_idx\x18\x04 \x01(\r\"\x85\x01\n\x06Policy\x12\x08\n\x04NONE\x10\x00\x12\n\n\x06PUBLIC\x10\x01\x12\n\n\x06SYSTEM\x10\x02\x12\n\n\x06VENDOR\x10\x03\x12\x0b\n\x07PRODUCT\x10\x04\x12\r\n\tSIGNATURE\x10\x05\x12\x07\n\x03ODM\x10\x06\x12\x07\n\x03OEM\x10\x07\x12\t\n\x05\x41\x43TOR\x10\x08\x12\x14\n\x10\x43ONFIG_SIGNATURE\x10\t\">\n\x08StagedId\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x11\n\tstaged_id\x18\x02 \x01(\r\"\x15\n\x07\x45ntryId\x12\n\n\x02id\x18\x01 \x01(\r\"\xc8\x02\n\x05\x45ntry\x12\"\n\x08\x65ntry_id\x18\x01 \x01(\x0b\x32\x10.aapt.pb.EntryId\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\'\n\nvisibility\x18\x03 \x01(\x0b\x32\x13.aapt.pb.Visibility\x12$\n\tallow_new\x18\x04 \x01(\x0b\x32\x11.aapt.pb.AllowNew\x12\x32\n\x10overlayable_item\x18\x05 \x01(\x0b\x32\x18.aapt.pb.OverlayableItem\x12*\n\x0c\x63onfig_value\x18\x06 \x03(\x0b\x32\x14.aapt.pb.ConfigValue\x12$\n\tstaged_id\x18\x07 \x01(\x0b\x32\x11.aapt.pb.StagedId\x12\x38\n\x1a\x66lag_disabled_config_value\x18\x08 \x03(\x0b\x32\x14.aapt.pb.ConfigValue\"Z\n\x0b\x43onfigValue\x12&\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\x16.aapt.pb.Configuration\x12\x1d\n\x05value\x18\x02 \x01(\x0b\x32\x0e.aapt.pb.ValueJ\x04\x08\x03\x10\x04\"\xa1\x01\n\x05Value\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12\x0c\n\x04weak\x18\x03 \x01(\x08\x12\x1d\n\x04item\x18\x04 \x01(\x0b\x32\r.aapt.pb.ItemH\x00\x12\x30\n\x0e\x63ompound_value\x18\x05 \x01(\x0b\x32\x16.aapt.pb.CompoundValueH\x00\x42\x07\n\x05value\"\xcb\x02\n\x04Item\x12!\n\x03ref\x18\x01 \x01(\x0b\x32\x12.aapt.pb.ReferenceH\x00\x12\x1e\n\x03str\x18\x02 \x01(\x0b\x32\x0f.aapt.pb.StringH\x00\x12%\n\x07raw_str\x18\x03 \x01(\x0b\x32\x12.aapt.pb.RawStringH\x00\x12+\n\nstyled_str\x18\x04 \x01(\x0b\x32\x15.aapt.pb.StyledStringH\x00\x12&\n\x04\x66ile\x18\x05 \x01(\x0b\x32\x16.aapt.pb.FileReferenceH\x00\x12\x19\n\x02id\x18\x06 \x01(\x0b\x32\x0b.aapt.pb.IdH\x00\x12\"\n\x04prim\x18\x07 \x01(\x0b\x32\x12.aapt.pb.PrimitiveH\x00\x12\x13\n\x0b\x66lag_status\x18\x08 \x01(\r\x12\x14\n\x0c\x66lag_negated\x18\t \x01(\x08\x12\x11\n\tflag_name\x18\n \x01(\tB\x07\n\x05value\"\xad\x02\n\rCompoundValue\x12\"\n\x04\x61ttr\x18\x01 \x01(\x0b\x32\x12.aapt.pb.AttributeH\x00\x12\x1f\n\x05style\x18\x02 \x01(\x0b\x32\x0e.aapt.pb.StyleH\x00\x12\'\n\tstyleable\x18\x03 \x01(\x0b\x32\x12.aapt.pb.StyleableH\x00\x12\x1f\n\x05\x61rray\x18\x04 \x01(\x0b\x32\x0e.aapt.pb.ArrayH\x00\x12!\n\x06plural\x18\x05 \x01(\x0b\x32\x0f.aapt.pb.PluralH\x00\x12#\n\x05macro\x18\x06 \x01(\x0b\x32\x12.aapt.pb.MacroBodyH\x00\x12\x13\n\x0b\x66lag_status\x18\x07 \x01(\r\x12\x14\n\x0c\x66lag_negated\x18\x08 \x01(\x08\x12\x11\n\tflag_name\x18\t \x01(\tB\x07\n\x05value\"\x18\n\x07\x42oolean\x12\r\n\x05value\x18\x01 \x01(\x08\"\xd0\x01\n\tReference\x12%\n\x04type\x18\x01 \x01(\x0e\x32\x17.aapt.pb.Reference.Type\x12\n\n\x02id\x18\x02 \x01(\r\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0f\n\x07private\x18\x04 \x01(\x08\x12$\n\nis_dynamic\x18\x05 \x01(\x0b\x32\x10.aapt.pb.Boolean\x12\x12\n\ntype_flags\x18\x06 \x01(\r\x12\x11\n\tallow_raw\x18\x07 \x01(\x08\"$\n\x04Type\x12\r\n\tREFERENCE\x10\x00\x12\r\n\tATTRIBUTE\x10\x01\"\x04\n\x02Id\"\x17\n\x06String\x12\r\n\x05value\x18\x01 \x01(\t\"\x1a\n\tRawString\x12\r\n\x05value\x18\x01 \x01(\t\"\x83\x01\n\x0cStyledString\x12\r\n\x05value\x18\x01 \x01(\t\x12(\n\x04span\x18\x02 \x03(\x0b\x32\x1a.aapt.pb.StyledString.Span\x1a:\n\x04Span\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x12\n\nfirst_char\x18\x02 \x01(\r\x12\x11\n\tlast_char\x18\x03 \x01(\r\"\x85\x01\n\rFileReference\x12\x0c\n\x04path\x18\x01 \x01(\t\x12)\n\x04type\x18\x02 \x01(\x0e\x32\x1b.aapt.pb.FileReference.Type\";\n\x04Type\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x07\n\x03PNG\x10\x01\x12\x0e\n\nBINARY_XML\x10\x02\x12\r\n\tPROTO_XML\x10\x03\"\x83\x04\n\tPrimitive\x12\x31\n\nnull_value\x18\x01 \x01(\x0b\x32\x1b.aapt.pb.Primitive.NullTypeH\x00\x12\x33\n\x0b\x65mpty_value\x18\x02 \x01(\x0b\x32\x1c.aapt.pb.Primitive.EmptyTypeH\x00\x12\x15\n\x0b\x66loat_value\x18\x03 \x01(\x02H\x00\x12\x19\n\x0f\x64imension_value\x18\r \x01(\rH\x00\x12\x18\n\x0e\x66raction_value\x18\x0e \x01(\rH\x00\x12\x1b\n\x11int_decimal_value\x18\x06 \x01(\x05H\x00\x12\x1f\n\x15int_hexadecimal_value\x18\x07 \x01(\rH\x00\x12\x17\n\rboolean_value\x18\x08 \x01(\x08H\x00\x12\x1b\n\x11\x63olor_argb8_value\x18\t \x01(\rH\x00\x12\x1a\n\x10\x63olor_rgb8_value\x18\n \x01(\rH\x00\x12\x1b\n\x11\x63olor_argb4_value\x18\x0b \x01(\rH\x00\x12\x1a\n\x10\x63olor_rgb4_value\x18\x0c \x01(\rH\x00\x12(\n\x1a\x64imension_value_deprecated\x18\x04 \x01(\x02\x42\x02\x18\x01H\x00\x12\'\n\x19\x66raction_value_deprecated\x18\x05 \x01(\x02\x42\x02\x18\x01H\x00\x1a\n\n\x08NullType\x1a\x0b\n\tEmptyTypeB\r\n\x0boneof_value\"\x90\x03\n\tAttribute\x12\x14\n\x0c\x66ormat_flags\x18\x01 \x01(\r\x12\x0f\n\x07min_int\x18\x02 \x01(\x05\x12\x0f\n\x07max_int\x18\x03 \x01(\x05\x12)\n\x06symbol\x18\x04 \x03(\x0b\x32\x19.aapt.pb.Attribute.Symbol\x1ay\n\x06Symbol\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12 \n\x04name\x18\x03 \x01(\x0b\x32\x12.aapt.pb.Reference\x12\r\n\x05value\x18\x04 \x01(\r\x12\x0c\n\x04type\x18\x05 \x01(\r\"\xa4\x01\n\x0b\x46ormatFlags\x12\x08\n\x04NONE\x10\x00\x12\t\n\x03\x41NY\x10\xff\xff\x03\x12\r\n\tREFERENCE\x10\x01\x12\n\n\x06STRING\x10\x02\x12\x0b\n\x07INTEGER\x10\x04\x12\x0b\n\x07\x42OOLEAN\x10\x08\x12\t\n\x05\x43OLOR\x10\x10\x12\t\n\x05\x46LOAT\x10 \x12\r\n\tDIMENSION\x10@\x12\r\n\x08\x46RACTION\x10\x80\x01\x12\n\n\x04\x45NUM\x10\x80\x80\x04\x12\x0b\n\x05\x46LAGS\x10\x80\x80\x08\"\xf1\x01\n\x05Style\x12\"\n\x06parent\x18\x01 \x01(\x0b\x32\x12.aapt.pb.Reference\x12&\n\rparent_source\x18\x02 \x01(\x0b\x32\x0f.aapt.pb.Source\x12#\n\x05\x65ntry\x18\x03 \x03(\x0b\x32\x14.aapt.pb.Style.Entry\x1aw\n\x05\x45ntry\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12\x1f\n\x03key\x18\x03 \x01(\x0b\x32\x12.aapt.pb.Reference\x12\x1b\n\x04item\x18\x04 \x01(\x0b\x32\r.aapt.pb.Item\"\x91\x01\n\tStyleable\x12\'\n\x05\x65ntry\x18\x01 \x03(\x0b\x32\x18.aapt.pb.Styleable.Entry\x1a[\n\x05\x45ntry\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12 \n\x04\x61ttr\x18\x03 \x01(\x0b\x32\x12.aapt.pb.Reference\"\x8a\x01\n\x05\x41rray\x12\'\n\x07\x65lement\x18\x01 \x03(\x0b\x32\x16.aapt.pb.Array.Element\x1aX\n\x07\x45lement\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12\x1b\n\x04item\x18\x03 \x01(\x0b\x32\r.aapt.pb.Item\"\xef\x01\n\x06Plural\x12$\n\x05\x65ntry\x18\x01 \x03(\x0b\x32\x15.aapt.pb.Plural.Entry\x1a|\n\x05\x45ntry\x12\x1f\n\x06source\x18\x01 \x01(\x0b\x32\x0f.aapt.pb.Source\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\x12$\n\x05\x61rity\x18\x03 \x01(\x0e\x32\x15.aapt.pb.Plural.Arity\x12\x1b\n\x04item\x18\x04 \x01(\x0b\x32\r.aapt.pb.Item\"A\n\x05\x41rity\x12\x08\n\x04ZERO\x10\x00\x12\x07\n\x03ONE\x10\x01\x12\x07\n\x03TWO\x10\x02\x12\x07\n\x03\x46\x45W\x10\x03\x12\x08\n\x04MANY\x10\x04\x12\t\n\x05OTHER\x10\x05\"r\n\x07XmlNode\x12&\n\x07\x65lement\x18\x01 \x01(\x0b\x32\x13.aapt.pb.XmlElementH\x00\x12\x0e\n\x04text\x18\x02 \x01(\tH\x00\x12\'\n\x06source\x18\x03 \x01(\x0b\x32\x17.aapt.pb.SourcePositionB\x06\n\x04node\"\xb2\x01\n\nXmlElement\x12\x34\n\x15namespace_declaration\x18\x01 \x03(\x0b\x32\x15.aapt.pb.XmlNamespace\x12\x15\n\rnamespace_uri\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12(\n\tattribute\x18\x04 \x03(\x0b\x32\x15.aapt.pb.XmlAttribute\x12\x1f\n\x05\x63hild\x18\x05 \x03(\x0b\x32\x10.aapt.pb.XmlNode\"T\n\x0cXmlNamespace\x12\x0e\n\x06prefix\x18\x01 \x01(\t\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\'\n\x06source\x18\x03 \x01(\x0b\x32\x17.aapt.pb.SourcePosition\"\xa6\x01\n\x0cXmlAttribute\x12\x15\n\rnamespace_uri\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\x12\'\n\x06source\x18\x04 \x01(\x0b\x32\x17.aapt.pb.SourcePosition\x12\x13\n\x0bresource_id\x18\x05 \x01(\r\x12$\n\rcompiled_item\x18\x06 \x01(\x0b\x32\r.aapt.pb.Item\"\xe7\x01\n\tMacroBody\x12\x12\n\nraw_string\x18\x01 \x01(\t\x12*\n\x0cstyle_string\x18\x02 \x01(\x0b\x32\x14.aapt.pb.StyleString\x12?\n\x17untranslatable_sections\x18\x03 \x03(\x0b\x32\x1e.aapt.pb.UntranslatableSection\x12\x30\n\x0fnamespace_stack\x18\x04 \x03(\x0b\x32\x17.aapt.pb.NamespaceAlias\x12\'\n\x06source\x18\x05 \x01(\x0b\x32\x17.aapt.pb.SourcePosition\"J\n\x0eNamespaceAlias\x12\x0e\n\x06prefix\x18\x01 \x01(\t\x12\x14\n\x0cpackage_name\x18\x02 \x01(\t\x12\x12\n\nis_private\x18\x03 \x01(\x08\"\x82\x01\n\x0bStyleString\x12\x0b\n\x03str\x18\x01 \x01(\t\x12(\n\x05spans\x18\x02 \x03(\x0b\x32\x19.aapt.pb.StyleString.Span\x1a<\n\x04Span\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bstart_index\x18\x02 \x01(\r\x12\x11\n\tend_index\x18\x03 \x01(\r\"?\n\x15UntranslatableSection\x12\x13\n\x0bstart_index\x18\x01 \x01(\x04\x12\x11\n\tend_index\x18\x02 \x01(\x04\x42\x12\n\x10\x63om.android.aaptb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'frameworks.base.tools.aapt2.Resources_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020com.android.aapt' + _globals['_PRIMITIVE'].fields_by_name['dimension_value_deprecated']._loaded_options = None + _globals['_PRIMITIVE'].fields_by_name['dimension_value_deprecated']._serialized_options = b'\030\001' + _globals['_PRIMITIVE'].fields_by_name['fraction_value_deprecated']._loaded_options = None + _globals['_PRIMITIVE'].fields_by_name['fraction_value_deprecated']._serialized_options = b'\030\001' + _globals['_STRINGPOOL']._serialized_start=105 + _globals['_STRINGPOOL']._serialized_end=131 + _globals['_SOURCEPOSITION']._serialized_start=133 + _globals['_SOURCEPOSITION']._serialized_end=193 + _globals['_SOURCE']._serialized_start=195 + _globals['_SOURCE']._serialized_end=264 + _globals['_TOOLFINGERPRINT']._serialized_start=266 + _globals['_TOOLFINGERPRINT']._serialized_end=314 + _globals['_DYNAMICREFTABLE']._serialized_start=316 + _globals['_DYNAMICREFTABLE']._serialized_end=395 + _globals['_RESOURCETABLE']._serialized_start=398 + _globals['_RESOURCETABLE']._serialized_end=638 + _globals['_PACKAGEID']._serialized_start=640 + _globals['_PACKAGEID']._serialized_end=663 + _globals['_PACKAGE']._serialized_start=665 + _globals['_PACKAGE']._serialized_end=765 + _globals['_TYPEID']._serialized_start=767 + _globals['_TYPEID']._serialized_end=787 + _globals['_TYPE']._serialized_start=789 + _globals['_TYPE']._serialized_end=874 + _globals['_VISIBILITY']._serialized_start=877 + _globals['_VISIBILITY']._serialized_end=1048 + _globals['_VISIBILITY_LEVEL']._serialized_start=1003 + _globals['_VISIBILITY_LEVEL']._serialized_end=1048 + _globals['_ALLOWNEW']._serialized_start=1050 + _globals['_ALLOWNEW']._serialized_end=1110 + _globals['_OVERLAYABLE']._serialized_start=1112 + _globals['_OVERLAYABLE']._serialized_end=1187 + _globals['_OVERLAYABLEITEM']._serialized_start=1190 + _globals['_OVERLAYABLEITEM']._serialized_end=1467 + _globals['_OVERLAYABLEITEM_POLICY']._serialized_start=1334 + _globals['_OVERLAYABLEITEM_POLICY']._serialized_end=1467 + _globals['_STAGEDID']._serialized_start=1469 + _globals['_STAGEDID']._serialized_end=1531 + _globals['_ENTRYID']._serialized_start=1533 + _globals['_ENTRYID']._serialized_end=1554 + _globals['_ENTRY']._serialized_start=1557 + _globals['_ENTRY']._serialized_end=1885 + _globals['_CONFIGVALUE']._serialized_start=1887 + _globals['_CONFIGVALUE']._serialized_end=1977 + _globals['_VALUE']._serialized_start=1980 + _globals['_VALUE']._serialized_end=2141 + _globals['_ITEM']._serialized_start=2144 + _globals['_ITEM']._serialized_end=2475 + _globals['_COMPOUNDVALUE']._serialized_start=2478 + _globals['_COMPOUNDVALUE']._serialized_end=2779 + _globals['_BOOLEAN']._serialized_start=2781 + _globals['_BOOLEAN']._serialized_end=2805 + _globals['_REFERENCE']._serialized_start=2808 + _globals['_REFERENCE']._serialized_end=3016 + _globals['_REFERENCE_TYPE']._serialized_start=2980 + _globals['_REFERENCE_TYPE']._serialized_end=3016 + _globals['_ID']._serialized_start=3018 + _globals['_ID']._serialized_end=3022 + _globals['_STRING']._serialized_start=3024 + _globals['_STRING']._serialized_end=3047 + _globals['_RAWSTRING']._serialized_start=3049 + _globals['_RAWSTRING']._serialized_end=3075 + _globals['_STYLEDSTRING']._serialized_start=3078 + _globals['_STYLEDSTRING']._serialized_end=3209 + _globals['_STYLEDSTRING_SPAN']._serialized_start=3151 + _globals['_STYLEDSTRING_SPAN']._serialized_end=3209 + _globals['_FILEREFERENCE']._serialized_start=3212 + _globals['_FILEREFERENCE']._serialized_end=3345 + _globals['_FILEREFERENCE_TYPE']._serialized_start=3286 + _globals['_FILEREFERENCE_TYPE']._serialized_end=3345 + _globals['_PRIMITIVE']._serialized_start=3348 + _globals['_PRIMITIVE']._serialized_end=3863 + _globals['_PRIMITIVE_NULLTYPE']._serialized_start=3825 + _globals['_PRIMITIVE_NULLTYPE']._serialized_end=3835 + _globals['_PRIMITIVE_EMPTYTYPE']._serialized_start=3837 + _globals['_PRIMITIVE_EMPTYTYPE']._serialized_end=3848 + _globals['_ATTRIBUTE']._serialized_start=3866 + _globals['_ATTRIBUTE']._serialized_end=4266 + _globals['_ATTRIBUTE_SYMBOL']._serialized_start=3978 + _globals['_ATTRIBUTE_SYMBOL']._serialized_end=4099 + _globals['_ATTRIBUTE_FORMATFLAGS']._serialized_start=4102 + _globals['_ATTRIBUTE_FORMATFLAGS']._serialized_end=4266 + _globals['_STYLE']._serialized_start=4269 + _globals['_STYLE']._serialized_end=4510 + _globals['_STYLE_ENTRY']._serialized_start=4391 + _globals['_STYLE_ENTRY']._serialized_end=4510 + _globals['_STYLEABLE']._serialized_start=4513 + _globals['_STYLEABLE']._serialized_end=4658 + _globals['_STYLEABLE_ENTRY']._serialized_start=4567 + _globals['_STYLEABLE_ENTRY']._serialized_end=4658 + _globals['_ARRAY']._serialized_start=4661 + _globals['_ARRAY']._serialized_end=4799 + _globals['_ARRAY_ELEMENT']._serialized_start=4711 + _globals['_ARRAY_ELEMENT']._serialized_end=4799 + _globals['_PLURAL']._serialized_start=4802 + _globals['_PLURAL']._serialized_end=5041 + _globals['_PLURAL_ENTRY']._serialized_start=4850 + _globals['_PLURAL_ENTRY']._serialized_end=4974 + _globals['_PLURAL_ARITY']._serialized_start=4976 + _globals['_PLURAL_ARITY']._serialized_end=5041 + _globals['_XMLNODE']._serialized_start=5043 + _globals['_XMLNODE']._serialized_end=5157 + _globals['_XMLELEMENT']._serialized_start=5160 + _globals['_XMLELEMENT']._serialized_end=5338 + _globals['_XMLNAMESPACE']._serialized_start=5340 + _globals['_XMLNAMESPACE']._serialized_end=5424 + _globals['_XMLATTRIBUTE']._serialized_start=5427 + _globals['_XMLATTRIBUTE']._serialized_end=5593 + _globals['_MACROBODY']._serialized_start=5596 + _globals['_MACROBODY']._serialized_end=5827 + _globals['_NAMESPACEALIAS']._serialized_start=5829 + _globals['_NAMESPACEALIAS']._serialized_end=5903 + _globals['_STYLESTRING']._serialized_start=5906 + _globals['_STYLESTRING']._serialized_end=6036 + _globals['_STYLESTRING_SPAN']._serialized_start=5976 + _globals['_STYLESTRING_SPAN']._serialized_end=6036 + _globals['_UNTRANSLATABLESECTION']._serialized_start=6038 + _globals['_UNTRANSLATABLESECTION']._serialized_end=6101 # @@protoc_insertion_point(module_scope) diff --git a/direct/src/dist/_proto/config_pb2.py b/direct/src/dist/_proto/config_pb2.py index dde4c7165c..67a2c7cc95 100644 --- a/direct/src/dist/_proto/config_pb2.py +++ b/direct/src/dist/_proto/config_pb2.py @@ -1,11 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: config.proto +# Protobuf Python Version: 6.33.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'config.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -13,1033 +24,74 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor.FileDescriptor( - name='config.proto', - package='android.bundle', - syntax='proto3', - serialized_options=b'\n\022com.android.bundle', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x0c\x63onfig.proto\x12\x0e\x61ndroid.bundle\"\x91\x04\n\x0c\x42undleConfig\x12.\n\nbundletool\x18\x01 \x01(\x0b\x32\x1a.android.bundle.Bundletool\x12\x34\n\roptimizations\x18\x02 \x01(\x0b\x32\x1d.android.bundle.Optimizations\x12\x30\n\x0b\x63ompression\x18\x03 \x01(\x0b\x32\x1b.android.bundle.Compression\x12\x39\n\x10master_resources\x18\x04 \x01(\x0b\x32\x1f.android.bundle.MasterResources\x12/\n\x0b\x61pex_config\x18\x05 \x01(\x0b\x32\x1a.android.bundle.ApexConfig\x12O\n\x1cunsigned_embedded_apk_config\x18\x06 \x03(\x0b\x32).android.bundle.UnsignedEmbeddedApkConfig\x12@\n\x14\x61sset_modules_config\x18\x07 \x01(\x0b\x32\".android.bundle.AssetModulesConfig\x12\x35\n\x04type\x18\x08 \x01(\x0e\x32\'.android.bundle.BundleConfig.BundleType\"3\n\nBundleType\x12\x0b\n\x07REGULAR\x10\x00\x12\x08\n\x04\x41PEX\x10\x01\x12\x0e\n\nASSET_ONLY\x10\x02\"#\n\nBundletool\x12\x0f\n\x07version\x18\x02 \x01(\tJ\x04\x08\x01\x10\x02\"\xe0\x01\n\x0b\x43ompression\x12\x19\n\x11uncompressed_glob\x18\x01 \x03(\t\x12i\n-install_time_asset_module_default_compression\x18\x02 \x01(\x0e\x32\x32.android.bundle.Compression.AssetModuleCompression\"K\n\x16\x41ssetModuleCompression\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x10\n\x0cUNCOMPRESSED\x10\x01\x12\x0e\n\nCOMPRESSED\x10\x02\"?\n\x0fMasterResources\x12\x14\n\x0cresource_ids\x18\x01 \x03(\x05\x12\x16\n\x0eresource_names\x18\x02 \x03(\t\"\x8f\x03\n\rOptimizations\x12\x33\n\rsplits_config\x18\x01 \x01(\x0b\x32\x1c.android.bundle.SplitsConfig\x12N\n\x1buncompress_native_libraries\x18\x02 \x01(\x0b\x32).android.bundle.UncompressNativeLibraries\x12@\n\x14uncompress_dex_files\x18\x03 \x01(\x0b\x32\".android.bundle.UncompressDexFiles\x12;\n\x11standalone_config\x18\x04 \x01(\x0b\x32 .android.bundle.StandaloneConfig\x12\x45\n\x16resource_optimizations\x18\x05 \x01(\x0b\x32%.android.bundle.ResourceOptimizations\x12\x33\n\rstore_archive\x18\x06 \x01(\x0b\x32\x1c.android.bundle.StoreArchive\"\x97\x01\n\x15ResourceOptimizations\x12M\n\x0fsparse_encoding\x18\x01 \x01(\x0e\x32\x34.android.bundle.ResourceOptimizations.SparseEncoding\"/\n\x0eSparseEncoding\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0c\n\x08\x45NFORCED\x10\x01\",\n\x19UncompressNativeLibraries\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\"%\n\x12UncompressDexFiles\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\"\x1f\n\x0cStoreArchive\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\"G\n\x0cSplitsConfig\x12\x37\n\x0fsplit_dimension\x18\x01 \x03(\x0b\x32\x1e.android.bundle.SplitDimension\"\xfa\x01\n\x10StandaloneConfig\x12\x37\n\x0fsplit_dimension\x18\x01 \x03(\x0b\x32\x1e.android.bundle.SplitDimension\x12\x1e\n\x16strip_64_bit_libraries\x18\x02 \x01(\x08\x12Q\n\x14\x64\x65x_merging_strategy\x18\x03 \x01(\x0e\x32\x33.android.bundle.StandaloneConfig.DexMergingStrategy\":\n\x12\x44\x65xMergingStrategy\x12\x13\n\x0fMERGE_IF_NEEDED\x10\x00\x12\x0f\n\x0bNEVER_MERGE\x10\x01\"\x8c\x02\n\x0eSplitDimension\x12\x33\n\x05value\x18\x01 \x01(\x0e\x32$.android.bundle.SplitDimension.Value\x12\x0e\n\x06negate\x18\x02 \x01(\x08\x12\x39\n\x10suffix_stripping\x18\x03 \x01(\x0b\x32\x1f.android.bundle.SuffixStripping\"z\n\x05Value\x12\x15\n\x11UNSPECIFIED_VALUE\x10\x00\x12\x07\n\x03\x41\x42I\x10\x01\x12\x12\n\x0eSCREEN_DENSITY\x10\x02\x12\x0c\n\x08LANGUAGE\x10\x03\x12\x1e\n\x1aTEXTURE_COMPRESSION_FORMAT\x10\x04\x12\x0f\n\x0b\x44\x45VICE_TIER\x10\x06\":\n\x0fSuffixStripping\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x16\n\x0e\x64\x65\x66\x61ult_suffix\x18\x02 \x01(\t\"U\n\nApexConfig\x12G\n\x18\x61pex_embedded_apk_config\x18\x01 \x03(\x0b\x32%.android.bundle.ApexEmbeddedApkConfig\";\n\x15\x41pexEmbeddedApkConfig\x12\x14\n\x0cpackage_name\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\")\n\x19UnsignedEmbeddedApkConfig\x12\x0c\n\x04path\x18\x01 \x01(\t\"D\n\x12\x41ssetModulesConfig\x12\x13\n\x0b\x61pp_version\x18\x01 \x03(\x03\x12\x19\n\x11\x61sset_version_tag\x18\x02 \x01(\tB\x14\n\x12\x63om.android.bundleb\x06proto3' -) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63onfig.proto\x12\x0e\x61ndroid.bundle\"\xbb\x04\n\x0c\x42undleConfig\x12.\n\nbundletool\x18\x01 \x01(\x0b\x32\x1a.android.bundle.Bundletool\x12\x34\n\roptimizations\x18\x02 \x01(\x0b\x32\x1d.android.bundle.Optimizations\x12\x30\n\x0b\x63ompression\x18\x03 \x01(\x0b\x32\x1b.android.bundle.Compression\x12\x39\n\x10master_resources\x18\x04 \x01(\x0b\x32\x1f.android.bundle.MasterResources\x12/\n\x0b\x61pex_config\x18\x05 \x01(\x0b\x32\x1a.android.bundle.ApexConfig\x12O\n\x1cunsigned_embedded_apk_config\x18\x06 \x03(\x0b\x32).android.bundle.UnsignedEmbeddedApkConfig\x12@\n\x14\x61sset_modules_config\x18\x07 \x01(\x0b\x32\".android.bundle.AssetModulesConfig\x12\x35\n\x04type\x18\x08 \x01(\x0e\x32\'.android.bundle.BundleConfig.BundleType\x12(\n\x07locales\x18\t \x01(\x0b\x32\x17.android.bundle.Locales\"3\n\nBundleType\x12\x0b\n\x07REGULAR\x10\x00\x12\x08\n\x04\x41PEX\x10\x01\x12\x0e\n\nASSET_ONLY\x10\x02\"#\n\nBundletool\x12\x0f\n\x07version\x18\x02 \x01(\tJ\x04\x08\x01\x10\x02\"\x85\x03\n\x0b\x43ompression\x12\x19\n\x11uncompressed_glob\x18\x01 \x03(\t\x12i\n-install_time_asset_module_default_compression\x18\x02 \x01(\x0e\x32\x32.android.bundle.Compression.AssetModuleCompression\x12V\n\x19\x61pk_compression_algorithm\x18\x03 \x01(\x0e\x32\x33.android.bundle.Compression.ApkCompressionAlgorithm\"K\n\x16\x41ssetModuleCompression\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x10\n\x0cUNCOMPRESSED\x10\x01\x12\x0e\n\nCOMPRESSED\x10\x02\"K\n\x17\x41pkCompressionAlgorithm\x12%\n!DEFAULT_APK_COMPRESSION_ALGORITHM\x10\x00\x12\t\n\x05P7ZIP\x10\x01\"?\n\x0fMasterResources\x12\x14\n\x0cresource_ids\x18\x01 \x03(\x05\x12\x16\n\x0eresource_names\x18\x02 \x03(\t\"\xa7\x03\n\rOptimizations\x12\x33\n\rsplits_config\x18\x01 \x01(\x0b\x32\x1c.android.bundle.SplitsConfig\x12N\n\x1buncompress_native_libraries\x18\x02 \x01(\x0b\x32).android.bundle.UncompressNativeLibraries\x12@\n\x14uncompress_dex_files\x18\x03 \x01(\x0b\x32\".android.bundle.UncompressDexFiles\x12;\n\x11standalone_config\x18\x04 \x01(\x0b\x32 .android.bundle.StandaloneConfig\x12\x45\n\x16resource_optimizations\x18\x05 \x01(\x0b\x32%.android.bundle.ResourceOptimizations\x12\x33\n\rstore_archive\x18\x06 \x01(\x0b\x32\x1c.android.bundle.StoreArchive\x12\x16\n\x0einject_min_sdk\x18\x07 \x01(\x08\"\xa4\x04\n\x15ResourceOptimizations\x12M\n\x0fsparse_encoding\x18\x01 \x01(\x0e\x32\x34.android.bundle.ResourceOptimizations.SparseEncoding\x12^\n\x18\x63ollapsed_resource_names\x18\x02 \x01(\x0b\x32<.android.bundle.ResourceOptimizations.CollapsedResourceNames\x1a\x31\n\x13ResourceTypeAndName\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x1a\xdd\x01\n\x16\x43ollapsedResourceNames\x12\x1f\n\x17\x63ollapse_resource_names\x18\x01 \x01(\x08\x12X\n\x15no_collapse_resources\x18\x02 \x03(\x0b\x32\x39.android.bundle.ResourceOptimizations.ResourceTypeAndName\x12\"\n\x1ano_collapse_resource_types\x18\x04 \x03(\t\x12$\n\x1c\x64\x65\x64uplicate_resource_entries\x18\x03 \x01(\x08\"I\n\x0eSparseEncoding\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x16\n\x12VARIANT_FOR_SDK_32\x10\x02\"\x04\x08\x01\x10\x01*\x08\x45NFORCED\"\xf0\x01\n\x19UncompressNativeLibraries\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12J\n\talignment\x18\x02 \x01(\x0e\x32\x37.android.bundle.UncompressNativeLibraries.PageAlignment\"v\n\rPageAlignment\x12\x1e\n\x1aPAGE_ALIGNMENT_UNSPECIFIED\x10\x00\x12\x15\n\x11PAGE_ALIGNMENT_4K\x10\x01\x12\x16\n\x12PAGE_ALIGNMENT_16K\x10\x02\x12\x16\n\x12PAGE_ALIGNMENT_64K\x10\x03\"\xd2\x01\n\x12UncompressDexFiles\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12`\n\x1buncompressed_dex_target_sdk\x18\x02 \x01(\x0e\x32;.android.bundle.UncompressDexFiles.UncompressedDexTargetSdk\"I\n\x18UncompressedDexTargetSdk\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\n\n\x06SDK_31\x10\x01\x12\n\n\x06SDK_33\x10\x03\"\x04\x08\x02\x10\x02\"\x1f\n\x0cStoreArchive\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\"\'\n\x07Locales\x12\x1c\n\x14inject_locale_config\x18\x01 \x01(\x08\"G\n\x0cSplitsConfig\x12\x37\n\x0fsplit_dimension\x18\x01 \x03(\x0b\x32\x1e.android.bundle.SplitDimension\"\x9c\x03\n\x10StandaloneConfig\x12\x37\n\x0fsplit_dimension\x18\x01 \x03(\x0b\x32\x1e.android.bundle.SplitDimension\x12\x1e\n\x16strip_64_bit_libraries\x18\x02 \x01(\x08\x12Q\n\x14\x64\x65x_merging_strategy\x18\x03 \x01(\x0e\x32\x33.android.bundle.StandaloneConfig.DexMergingStrategy\x12Q\n\x14\x66\x65\x61ture_modules_mode\x18\x04 \x01(\x0e\x32\x33.android.bundle.StandaloneConfig.FeatureModulesMode\":\n\x12\x44\x65xMergingStrategy\x12\x13\n\x0fMERGE_IF_NEEDED\x10\x00\x12\x0f\n\x0bNEVER_MERGE\x10\x01\"M\n\x12\x46\x65\x61tureModulesMode\x12\x19\n\x15\x46USED_FEATURE_MODULES\x10\x00\x12\x1c\n\x18SEPARATE_FEATURE_MODULES\x10\x01\"\xca\x02\n\x0eSplitDimension\x12\x33\n\x05value\x18\x01 \x01(\x0e\x32$.android.bundle.SplitDimension.Value\x12\x0e\n\x06negate\x18\x02 \x01(\x08\x12\x39\n\x10suffix_stripping\x18\x03 \x01(\x0b\x32\x1f.android.bundle.SuffixStripping\"\xb7\x01\n\x05Value\x12\x15\n\x11UNSPECIFIED_VALUE\x10\x00\x12\x07\n\x03\x41\x42I\x10\x01\x12\x12\n\x0eSCREEN_DENSITY\x10\x02\x12\x0c\n\x08LANGUAGE\x10\x03\x12\x1e\n\x1aTEXTURE_COMPRESSION_FORMAT\x10\x04\x12\x13\n\x0b\x44\x45VICE_TIER\x10\x06\x1a\x02\x08\x01\x12\x0f\n\x0b\x43OUNTRY_SET\x10\x07\x12\x14\n\x10\x41I_MODEL_VERSION\x10\x08\x12\x10\n\x0c\x44\x45VICE_GROUP\x10\t\":\n\x0fSuffixStripping\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x16\n\x0e\x64\x65\x66\x61ult_suffix\x18\x02 \x01(\t\"\x91\x01\n\nApexConfig\x12G\n\x18\x61pex_embedded_apk_config\x18\x01 \x03(\x0b\x32%.android.bundle.ApexEmbeddedApkConfig\x12:\n\x11supported_abi_set\x18\x02 \x03(\x0b\x32\x1f.android.bundle.SupportedAbiSet\"\x1e\n\x0fSupportedAbiSet\x12\x0b\n\x03\x61\x62i\x18\x01 \x03(\t\";\n\x15\x41pexEmbeddedApkConfig\x12\x14\n\x0cpackage_name\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\")\n\x19UnsignedEmbeddedApkConfig\x12\x0c\n\x04path\x18\x01 \x01(\t\"D\n\x12\x41ssetModulesConfig\x12\x13\n\x0b\x61pp_version\x18\x01 \x03(\x03\x12\x19\n\x11\x61sset_version_tag\x18\x02 \x01(\tB\x14\n\x12\x63om.android.bundleb\x06proto3') - - -_BUNDLECONFIG_BUNDLETYPE = _descriptor.EnumDescriptor( - name='BundleType', - full_name='android.bundle.BundleConfig.BundleType', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='REGULAR', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='APEX', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ASSET_ONLY', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=511, - serialized_end=562, -) -_sym_db.RegisterEnumDescriptor(_BUNDLECONFIG_BUNDLETYPE) - -_COMPRESSION_ASSETMODULECOMPRESSION = _descriptor.EnumDescriptor( - name='AssetModuleCompression', - full_name='android.bundle.Compression.AssetModuleCompression', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNSPECIFIED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UNCOMPRESSED', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='COMPRESSED', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=751, - serialized_end=826, -) -_sym_db.RegisterEnumDescriptor(_COMPRESSION_ASSETMODULECOMPRESSION) - -_RESOURCEOPTIMIZATIONS_SPARSEENCODING = _descriptor.EnumDescriptor( - name='SparseEncoding', - full_name='android.bundle.ResourceOptimizations.SparseEncoding', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNSPECIFIED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ENFORCED', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1400, - serialized_end=1447, -) -_sym_db.RegisterEnumDescriptor(_RESOURCEOPTIMIZATIONS_SPARSEENCODING) - -_STANDALONECONFIG_DEXMERGINGSTRATEGY = _descriptor.EnumDescriptor( - name='DexMergingStrategy', - full_name='android.bundle.StandaloneConfig.DexMergingStrategy', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='MERGE_IF_NEEDED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NEVER_MERGE', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1833, - serialized_end=1891, -) -_sym_db.RegisterEnumDescriptor(_STANDALONECONFIG_DEXMERGINGSTRATEGY) - -_SPLITDIMENSION_VALUE = _descriptor.EnumDescriptor( - name='Value', - full_name='android.bundle.SplitDimension.Value', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNSPECIFIED_VALUE', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ABI', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='SCREEN_DENSITY', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='LANGUAGE', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TEXTURE_COMPRESSION_FORMAT', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DEVICE_TIER', index=5, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2040, - serialized_end=2162, -) -_sym_db.RegisterEnumDescriptor(_SPLITDIMENSION_VALUE) - - -_BUNDLECONFIG = _descriptor.Descriptor( - name='BundleConfig', - full_name='android.bundle.BundleConfig', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='bundletool', full_name='android.bundle.BundleConfig.bundletool', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='optimizations', full_name='android.bundle.BundleConfig.optimizations', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='compression', full_name='android.bundle.BundleConfig.compression', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='master_resources', full_name='android.bundle.BundleConfig.master_resources', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='apex_config', full_name='android.bundle.BundleConfig.apex_config', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='unsigned_embedded_apk_config', full_name='android.bundle.BundleConfig.unsigned_embedded_apk_config', index=5, - number=6, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='asset_modules_config', full_name='android.bundle.BundleConfig.asset_modules_config', index=6, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='type', full_name='android.bundle.BundleConfig.type', index=7, - number=8, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _BUNDLECONFIG_BUNDLETYPE, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=33, - serialized_end=562, -) - - -_BUNDLETOOL = _descriptor.Descriptor( - name='Bundletool', - full_name='android.bundle.Bundletool', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='version', full_name='android.bundle.Bundletool.version', index=0, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=564, - serialized_end=599, -) - - -_COMPRESSION = _descriptor.Descriptor( - name='Compression', - full_name='android.bundle.Compression', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='uncompressed_glob', full_name='android.bundle.Compression.uncompressed_glob', index=0, - number=1, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='install_time_asset_module_default_compression', full_name='android.bundle.Compression.install_time_asset_module_default_compression', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _COMPRESSION_ASSETMODULECOMPRESSION, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=602, - serialized_end=826, -) - - -_MASTERRESOURCES = _descriptor.Descriptor( - name='MasterResources', - full_name='android.bundle.MasterResources', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='resource_ids', full_name='android.bundle.MasterResources.resource_ids', index=0, - number=1, type=5, cpp_type=1, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='resource_names', full_name='android.bundle.MasterResources.resource_names', index=1, - number=2, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=828, - serialized_end=891, -) - - -_OPTIMIZATIONS = _descriptor.Descriptor( - name='Optimizations', - full_name='android.bundle.Optimizations', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='splits_config', full_name='android.bundle.Optimizations.splits_config', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uncompress_native_libraries', full_name='android.bundle.Optimizations.uncompress_native_libraries', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uncompress_dex_files', full_name='android.bundle.Optimizations.uncompress_dex_files', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='standalone_config', full_name='android.bundle.Optimizations.standalone_config', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='resource_optimizations', full_name='android.bundle.Optimizations.resource_optimizations', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='store_archive', full_name='android.bundle.Optimizations.store_archive', index=5, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=894, - serialized_end=1293, -) - - -_RESOURCEOPTIMIZATIONS = _descriptor.Descriptor( - name='ResourceOptimizations', - full_name='android.bundle.ResourceOptimizations', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='sparse_encoding', full_name='android.bundle.ResourceOptimizations.sparse_encoding', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _RESOURCEOPTIMIZATIONS_SPARSEENCODING, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1296, - serialized_end=1447, -) - - -_UNCOMPRESSNATIVELIBRARIES = _descriptor.Descriptor( - name='UncompressNativeLibraries', - full_name='android.bundle.UncompressNativeLibraries', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='enabled', full_name='android.bundle.UncompressNativeLibraries.enabled', index=0, - number=1, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1449, - serialized_end=1493, -) - - -_UNCOMPRESSDEXFILES = _descriptor.Descriptor( - name='UncompressDexFiles', - full_name='android.bundle.UncompressDexFiles', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='enabled', full_name='android.bundle.UncompressDexFiles.enabled', index=0, - number=1, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1495, - serialized_end=1532, -) - - -_STOREARCHIVE = _descriptor.Descriptor( - name='StoreArchive', - full_name='android.bundle.StoreArchive', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='enabled', full_name='android.bundle.StoreArchive.enabled', index=0, - number=1, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1534, - serialized_end=1565, -) - - -_SPLITSCONFIG = _descriptor.Descriptor( - name='SplitsConfig', - full_name='android.bundle.SplitsConfig', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='split_dimension', full_name='android.bundle.SplitsConfig.split_dimension', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1567, - serialized_end=1638, -) - - -_STANDALONECONFIG = _descriptor.Descriptor( - name='StandaloneConfig', - full_name='android.bundle.StandaloneConfig', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='split_dimension', full_name='android.bundle.StandaloneConfig.split_dimension', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='strip_64_bit_libraries', full_name='android.bundle.StandaloneConfig.strip_64_bit_libraries', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dex_merging_strategy', full_name='android.bundle.StandaloneConfig.dex_merging_strategy', index=2, - number=3, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _STANDALONECONFIG_DEXMERGINGSTRATEGY, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1641, - serialized_end=1891, -) - - -_SPLITDIMENSION = _descriptor.Descriptor( - name='SplitDimension', - full_name='android.bundle.SplitDimension', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.SplitDimension.value', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='negate', full_name='android.bundle.SplitDimension.negate', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='suffix_stripping', full_name='android.bundle.SplitDimension.suffix_stripping', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _SPLITDIMENSION_VALUE, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1894, - serialized_end=2162, -) - - -_SUFFIXSTRIPPING = _descriptor.Descriptor( - name='SuffixStripping', - full_name='android.bundle.SuffixStripping', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='enabled', full_name='android.bundle.SuffixStripping.enabled', index=0, - number=1, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='default_suffix', full_name='android.bundle.SuffixStripping.default_suffix', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2164, - serialized_end=2222, -) - - -_APEXCONFIG = _descriptor.Descriptor( - name='ApexConfig', - full_name='android.bundle.ApexConfig', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='apex_embedded_apk_config', full_name='android.bundle.ApexConfig.apex_embedded_apk_config', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2224, - serialized_end=2309, -) - - -_APEXEMBEDDEDAPKCONFIG = _descriptor.Descriptor( - name='ApexEmbeddedApkConfig', - full_name='android.bundle.ApexEmbeddedApkConfig', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='package_name', full_name='android.bundle.ApexEmbeddedApkConfig.package_name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='path', full_name='android.bundle.ApexEmbeddedApkConfig.path', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2311, - serialized_end=2370, -) - - -_UNSIGNEDEMBEDDEDAPKCONFIG = _descriptor.Descriptor( - name='UnsignedEmbeddedApkConfig', - full_name='android.bundle.UnsignedEmbeddedApkConfig', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='android.bundle.UnsignedEmbeddedApkConfig.path', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2372, - serialized_end=2413, -) - - -_ASSETMODULESCONFIG = _descriptor.Descriptor( - name='AssetModulesConfig', - full_name='android.bundle.AssetModulesConfig', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='app_version', full_name='android.bundle.AssetModulesConfig.app_version', index=0, - number=1, type=3, cpp_type=2, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='asset_version_tag', full_name='android.bundle.AssetModulesConfig.asset_version_tag', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2415, - serialized_end=2483, -) - -_BUNDLECONFIG.fields_by_name['bundletool'].message_type = _BUNDLETOOL -_BUNDLECONFIG.fields_by_name['optimizations'].message_type = _OPTIMIZATIONS -_BUNDLECONFIG.fields_by_name['compression'].message_type = _COMPRESSION -_BUNDLECONFIG.fields_by_name['master_resources'].message_type = _MASTERRESOURCES -_BUNDLECONFIG.fields_by_name['apex_config'].message_type = _APEXCONFIG -_BUNDLECONFIG.fields_by_name['unsigned_embedded_apk_config'].message_type = _UNSIGNEDEMBEDDEDAPKCONFIG -_BUNDLECONFIG.fields_by_name['asset_modules_config'].message_type = _ASSETMODULESCONFIG -_BUNDLECONFIG.fields_by_name['type'].enum_type = _BUNDLECONFIG_BUNDLETYPE -_BUNDLECONFIG_BUNDLETYPE.containing_type = _BUNDLECONFIG -_COMPRESSION.fields_by_name['install_time_asset_module_default_compression'].enum_type = _COMPRESSION_ASSETMODULECOMPRESSION -_COMPRESSION_ASSETMODULECOMPRESSION.containing_type = _COMPRESSION -_OPTIMIZATIONS.fields_by_name['splits_config'].message_type = _SPLITSCONFIG -_OPTIMIZATIONS.fields_by_name['uncompress_native_libraries'].message_type = _UNCOMPRESSNATIVELIBRARIES -_OPTIMIZATIONS.fields_by_name['uncompress_dex_files'].message_type = _UNCOMPRESSDEXFILES -_OPTIMIZATIONS.fields_by_name['standalone_config'].message_type = _STANDALONECONFIG -_OPTIMIZATIONS.fields_by_name['resource_optimizations'].message_type = _RESOURCEOPTIMIZATIONS -_OPTIMIZATIONS.fields_by_name['store_archive'].message_type = _STOREARCHIVE -_RESOURCEOPTIMIZATIONS.fields_by_name['sparse_encoding'].enum_type = _RESOURCEOPTIMIZATIONS_SPARSEENCODING -_RESOURCEOPTIMIZATIONS_SPARSEENCODING.containing_type = _RESOURCEOPTIMIZATIONS -_SPLITSCONFIG.fields_by_name['split_dimension'].message_type = _SPLITDIMENSION -_STANDALONECONFIG.fields_by_name['split_dimension'].message_type = _SPLITDIMENSION -_STANDALONECONFIG.fields_by_name['dex_merging_strategy'].enum_type = _STANDALONECONFIG_DEXMERGINGSTRATEGY -_STANDALONECONFIG_DEXMERGINGSTRATEGY.containing_type = _STANDALONECONFIG -_SPLITDIMENSION.fields_by_name['value'].enum_type = _SPLITDIMENSION_VALUE -_SPLITDIMENSION.fields_by_name['suffix_stripping'].message_type = _SUFFIXSTRIPPING -_SPLITDIMENSION_VALUE.containing_type = _SPLITDIMENSION -_APEXCONFIG.fields_by_name['apex_embedded_apk_config'].message_type = _APEXEMBEDDEDAPKCONFIG -DESCRIPTOR.message_types_by_name['BundleConfig'] = _BUNDLECONFIG -DESCRIPTOR.message_types_by_name['Bundletool'] = _BUNDLETOOL -DESCRIPTOR.message_types_by_name['Compression'] = _COMPRESSION -DESCRIPTOR.message_types_by_name['MasterResources'] = _MASTERRESOURCES -DESCRIPTOR.message_types_by_name['Optimizations'] = _OPTIMIZATIONS -DESCRIPTOR.message_types_by_name['ResourceOptimizations'] = _RESOURCEOPTIMIZATIONS -DESCRIPTOR.message_types_by_name['UncompressNativeLibraries'] = _UNCOMPRESSNATIVELIBRARIES -DESCRIPTOR.message_types_by_name['UncompressDexFiles'] = _UNCOMPRESSDEXFILES -DESCRIPTOR.message_types_by_name['StoreArchive'] = _STOREARCHIVE -DESCRIPTOR.message_types_by_name['SplitsConfig'] = _SPLITSCONFIG -DESCRIPTOR.message_types_by_name['StandaloneConfig'] = _STANDALONECONFIG -DESCRIPTOR.message_types_by_name['SplitDimension'] = _SPLITDIMENSION -DESCRIPTOR.message_types_by_name['SuffixStripping'] = _SUFFIXSTRIPPING -DESCRIPTOR.message_types_by_name['ApexConfig'] = _APEXCONFIG -DESCRIPTOR.message_types_by_name['ApexEmbeddedApkConfig'] = _APEXEMBEDDEDAPKCONFIG -DESCRIPTOR.message_types_by_name['UnsignedEmbeddedApkConfig'] = _UNSIGNEDEMBEDDEDAPKCONFIG -DESCRIPTOR.message_types_by_name['AssetModulesConfig'] = _ASSETMODULESCONFIG -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -BundleConfig = _reflection.GeneratedProtocolMessageType('BundleConfig', (_message.Message,), { - 'DESCRIPTOR' : _BUNDLECONFIG, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.BundleConfig) - }) -_sym_db.RegisterMessage(BundleConfig) - -Bundletool = _reflection.GeneratedProtocolMessageType('Bundletool', (_message.Message,), { - 'DESCRIPTOR' : _BUNDLETOOL, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.Bundletool) - }) -_sym_db.RegisterMessage(Bundletool) - -Compression = _reflection.GeneratedProtocolMessageType('Compression', (_message.Message,), { - 'DESCRIPTOR' : _COMPRESSION, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.Compression) - }) -_sym_db.RegisterMessage(Compression) - -MasterResources = _reflection.GeneratedProtocolMessageType('MasterResources', (_message.Message,), { - 'DESCRIPTOR' : _MASTERRESOURCES, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.MasterResources) - }) -_sym_db.RegisterMessage(MasterResources) - -Optimizations = _reflection.GeneratedProtocolMessageType('Optimizations', (_message.Message,), { - 'DESCRIPTOR' : _OPTIMIZATIONS, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.Optimizations) - }) -_sym_db.RegisterMessage(Optimizations) - -ResourceOptimizations = _reflection.GeneratedProtocolMessageType('ResourceOptimizations', (_message.Message,), { - 'DESCRIPTOR' : _RESOURCEOPTIMIZATIONS, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.ResourceOptimizations) - }) -_sym_db.RegisterMessage(ResourceOptimizations) - -UncompressNativeLibraries = _reflection.GeneratedProtocolMessageType('UncompressNativeLibraries', (_message.Message,), { - 'DESCRIPTOR' : _UNCOMPRESSNATIVELIBRARIES, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.UncompressNativeLibraries) - }) -_sym_db.RegisterMessage(UncompressNativeLibraries) - -UncompressDexFiles = _reflection.GeneratedProtocolMessageType('UncompressDexFiles', (_message.Message,), { - 'DESCRIPTOR' : _UNCOMPRESSDEXFILES, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.UncompressDexFiles) - }) -_sym_db.RegisterMessage(UncompressDexFiles) - -StoreArchive = _reflection.GeneratedProtocolMessageType('StoreArchive', (_message.Message,), { - 'DESCRIPTOR' : _STOREARCHIVE, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.StoreArchive) - }) -_sym_db.RegisterMessage(StoreArchive) - -SplitsConfig = _reflection.GeneratedProtocolMessageType('SplitsConfig', (_message.Message,), { - 'DESCRIPTOR' : _SPLITSCONFIG, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.SplitsConfig) - }) -_sym_db.RegisterMessage(SplitsConfig) - -StandaloneConfig = _reflection.GeneratedProtocolMessageType('StandaloneConfig', (_message.Message,), { - 'DESCRIPTOR' : _STANDALONECONFIG, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.StandaloneConfig) - }) -_sym_db.RegisterMessage(StandaloneConfig) - -SplitDimension = _reflection.GeneratedProtocolMessageType('SplitDimension', (_message.Message,), { - 'DESCRIPTOR' : _SPLITDIMENSION, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.SplitDimension) - }) -_sym_db.RegisterMessage(SplitDimension) - -SuffixStripping = _reflection.GeneratedProtocolMessageType('SuffixStripping', (_message.Message,), { - 'DESCRIPTOR' : _SUFFIXSTRIPPING, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.SuffixStripping) - }) -_sym_db.RegisterMessage(SuffixStripping) - -ApexConfig = _reflection.GeneratedProtocolMessageType('ApexConfig', (_message.Message,), { - 'DESCRIPTOR' : _APEXCONFIG, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.ApexConfig) - }) -_sym_db.RegisterMessage(ApexConfig) - -ApexEmbeddedApkConfig = _reflection.GeneratedProtocolMessageType('ApexEmbeddedApkConfig', (_message.Message,), { - 'DESCRIPTOR' : _APEXEMBEDDEDAPKCONFIG, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.ApexEmbeddedApkConfig) - }) -_sym_db.RegisterMessage(ApexEmbeddedApkConfig) - -UnsignedEmbeddedApkConfig = _reflection.GeneratedProtocolMessageType('UnsignedEmbeddedApkConfig', (_message.Message,), { - 'DESCRIPTOR' : _UNSIGNEDEMBEDDEDAPKCONFIG, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.UnsignedEmbeddedApkConfig) - }) -_sym_db.RegisterMessage(UnsignedEmbeddedApkConfig) - -AssetModulesConfig = _reflection.GeneratedProtocolMessageType('AssetModulesConfig', (_message.Message,), { - 'DESCRIPTOR' : _ASSETMODULESCONFIG, - '__module__' : 'config_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.AssetModulesConfig) - }) -_sym_db.RegisterMessage(AssetModulesConfig) - - -DESCRIPTOR._options = None +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'config_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\022com.android.bundle' + _globals['_SPLITDIMENSION_VALUE'].values_by_name["DEVICE_TIER"]._loaded_options = None + _globals['_SPLITDIMENSION_VALUE'].values_by_name["DEVICE_TIER"]._serialized_options = b'\010\001' + _globals['_BUNDLECONFIG']._serialized_start=33 + _globals['_BUNDLECONFIG']._serialized_end=604 + _globals['_BUNDLECONFIG_BUNDLETYPE']._serialized_start=553 + _globals['_BUNDLECONFIG_BUNDLETYPE']._serialized_end=604 + _globals['_BUNDLETOOL']._serialized_start=606 + _globals['_BUNDLETOOL']._serialized_end=641 + _globals['_COMPRESSION']._serialized_start=644 + _globals['_COMPRESSION']._serialized_end=1033 + _globals['_COMPRESSION_ASSETMODULECOMPRESSION']._serialized_start=881 + _globals['_COMPRESSION_ASSETMODULECOMPRESSION']._serialized_end=956 + _globals['_COMPRESSION_APKCOMPRESSIONALGORITHM']._serialized_start=958 + _globals['_COMPRESSION_APKCOMPRESSIONALGORITHM']._serialized_end=1033 + _globals['_MASTERRESOURCES']._serialized_start=1035 + _globals['_MASTERRESOURCES']._serialized_end=1098 + _globals['_OPTIMIZATIONS']._serialized_start=1101 + _globals['_OPTIMIZATIONS']._serialized_end=1524 + _globals['_RESOURCEOPTIMIZATIONS']._serialized_start=1527 + _globals['_RESOURCEOPTIMIZATIONS']._serialized_end=2075 + _globals['_RESOURCEOPTIMIZATIONS_RESOURCETYPEANDNAME']._serialized_start=1727 + _globals['_RESOURCEOPTIMIZATIONS_RESOURCETYPEANDNAME']._serialized_end=1776 + _globals['_RESOURCEOPTIMIZATIONS_COLLAPSEDRESOURCENAMES']._serialized_start=1779 + _globals['_RESOURCEOPTIMIZATIONS_COLLAPSEDRESOURCENAMES']._serialized_end=2000 + _globals['_RESOURCEOPTIMIZATIONS_SPARSEENCODING']._serialized_start=2002 + _globals['_RESOURCEOPTIMIZATIONS_SPARSEENCODING']._serialized_end=2075 + _globals['_UNCOMPRESSNATIVELIBRARIES']._serialized_start=2078 + _globals['_UNCOMPRESSNATIVELIBRARIES']._serialized_end=2318 + _globals['_UNCOMPRESSNATIVELIBRARIES_PAGEALIGNMENT']._serialized_start=2200 + _globals['_UNCOMPRESSNATIVELIBRARIES_PAGEALIGNMENT']._serialized_end=2318 + _globals['_UNCOMPRESSDEXFILES']._serialized_start=2321 + _globals['_UNCOMPRESSDEXFILES']._serialized_end=2531 + _globals['_UNCOMPRESSDEXFILES_UNCOMPRESSEDDEXTARGETSDK']._serialized_start=2458 + _globals['_UNCOMPRESSDEXFILES_UNCOMPRESSEDDEXTARGETSDK']._serialized_end=2531 + _globals['_STOREARCHIVE']._serialized_start=2533 + _globals['_STOREARCHIVE']._serialized_end=2564 + _globals['_LOCALES']._serialized_start=2566 + _globals['_LOCALES']._serialized_end=2605 + _globals['_SPLITSCONFIG']._serialized_start=2607 + _globals['_SPLITSCONFIG']._serialized_end=2678 + _globals['_STANDALONECONFIG']._serialized_start=2681 + _globals['_STANDALONECONFIG']._serialized_end=3093 + _globals['_STANDALONECONFIG_DEXMERGINGSTRATEGY']._serialized_start=2956 + _globals['_STANDALONECONFIG_DEXMERGINGSTRATEGY']._serialized_end=3014 + _globals['_STANDALONECONFIG_FEATUREMODULESMODE']._serialized_start=3016 + _globals['_STANDALONECONFIG_FEATUREMODULESMODE']._serialized_end=3093 + _globals['_SPLITDIMENSION']._serialized_start=3096 + _globals['_SPLITDIMENSION']._serialized_end=3426 + _globals['_SPLITDIMENSION_VALUE']._serialized_start=3243 + _globals['_SPLITDIMENSION_VALUE']._serialized_end=3426 + _globals['_SUFFIXSTRIPPING']._serialized_start=3428 + _globals['_SUFFIXSTRIPPING']._serialized_end=3486 + _globals['_APEXCONFIG']._serialized_start=3489 + _globals['_APEXCONFIG']._serialized_end=3634 + _globals['_SUPPORTEDABISET']._serialized_start=3636 + _globals['_SUPPORTEDABISET']._serialized_end=3666 + _globals['_APEXEMBEDDEDAPKCONFIG']._serialized_start=3668 + _globals['_APEXEMBEDDEDAPKCONFIG']._serialized_end=3727 + _globals['_UNSIGNEDEMBEDDEDAPKCONFIG']._serialized_start=3729 + _globals['_UNSIGNEDEMBEDDEDAPKCONFIG']._serialized_end=3770 + _globals['_ASSETMODULESCONFIG']._serialized_start=3772 + _globals['_ASSETMODULESCONFIG']._serialized_end=3840 # @@protoc_insertion_point(module_scope) diff --git a/direct/src/dist/_proto/files_pb2.py b/direct/src/dist/_proto/files_pb2.py index f4ac2a9275..368f088bbc 100644 --- a/direct/src/dist/_proto/files_pb2.py +++ b/direct/src/dist/_proto/files_pb2.py @@ -1,11 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: files.proto +# Protobuf Python Version: 6.33.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'files.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -14,294 +25,24 @@ _sym_db = _symbol_database.Default() from . import targeting_pb2 as targeting__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='files.proto', - package='android.bundle', - syntax='proto3', - serialized_options=b'\n\022com.android.bundle', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x0b\x66iles.proto\x12\x0e\x61ndroid.bundle\x1a\x0ftargeting.proto\"D\n\x06\x41ssets\x12:\n\tdirectory\x18\x01 \x03(\x0b\x32\'.android.bundle.TargetedAssetsDirectory\"M\n\x0fNativeLibraries\x12:\n\tdirectory\x18\x01 \x03(\x0b\x32\'.android.bundle.TargetedNativeDirectory\"D\n\nApexImages\x12\x30\n\x05image\x18\x01 \x03(\x0b\x32!.android.bundle.TargetedApexImageJ\x04\x08\x02\x10\x03\"d\n\x17TargetedAssetsDirectory\x12\x0c\n\x04path\x18\x01 \x01(\t\x12;\n\ttargeting\x18\x02 \x01(\x0b\x32(.android.bundle.AssetsDirectoryTargeting\"d\n\x17TargetedNativeDirectory\x12\x0c\n\x04path\x18\x01 \x01(\t\x12;\n\ttargeting\x18\x02 \x01(\x0b\x32(.android.bundle.NativeDirectoryTargeting\"q\n\x11TargetedApexImage\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x17\n\x0f\x62uild_info_path\x18\x03 \x01(\t\x12\x35\n\ttargeting\x18\x02 \x01(\x0b\x32\".android.bundle.ApexImageTargetingB\x14\n\x12\x63om.android.bundleb\x06proto3' - , - dependencies=[targeting__pb2.DESCRIPTOR,]) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x66iles.proto\x12\x0e\x61ndroid.bundle\x1a\x0ftargeting.proto\"D\n\x06\x41ssets\x12:\n\tdirectory\x18\x01 \x03(\x0b\x32\'.android.bundle.TargetedAssetsDirectory\"M\n\x0fNativeLibraries\x12:\n\tdirectory\x18\x01 \x03(\x0b\x32\'.android.bundle.TargetedNativeDirectory\"D\n\nApexImages\x12\x30\n\x05image\x18\x01 \x03(\x0b\x32!.android.bundle.TargetedApexImageJ\x04\x08\x02\x10\x03\"d\n\x17TargetedAssetsDirectory\x12\x0c\n\x04path\x18\x01 \x01(\t\x12;\n\ttargeting\x18\x02 \x01(\x0b\x32(.android.bundle.AssetsDirectoryTargeting\"d\n\x17TargetedNativeDirectory\x12\x0c\n\x04path\x18\x01 \x01(\t\x12;\n\ttargeting\x18\x02 \x01(\x0b\x32(.android.bundle.NativeDirectoryTargeting\"q\n\x11TargetedApexImage\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x17\n\x0f\x62uild_info_path\x18\x03 \x01(\t\x12\x35\n\ttargeting\x18\x02 \x01(\x0b\x32\".android.bundle.ApexImageTargetingB\x14\n\x12\x63om.android.bundleb\x06proto3') - - - -_ASSETS = _descriptor.Descriptor( - name='Assets', - full_name='android.bundle.Assets', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='directory', full_name='android.bundle.Assets.directory', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=48, - serialized_end=116, -) - - -_NATIVELIBRARIES = _descriptor.Descriptor( - name='NativeLibraries', - full_name='android.bundle.NativeLibraries', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='directory', full_name='android.bundle.NativeLibraries.directory', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=118, - serialized_end=195, -) - - -_APEXIMAGES = _descriptor.Descriptor( - name='ApexImages', - full_name='android.bundle.ApexImages', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='image', full_name='android.bundle.ApexImages.image', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=197, - serialized_end=265, -) - - -_TARGETEDASSETSDIRECTORY = _descriptor.Descriptor( - name='TargetedAssetsDirectory', - full_name='android.bundle.TargetedAssetsDirectory', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='android.bundle.TargetedAssetsDirectory.path', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='targeting', full_name='android.bundle.TargetedAssetsDirectory.targeting', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=267, - serialized_end=367, -) - - -_TARGETEDNATIVEDIRECTORY = _descriptor.Descriptor( - name='TargetedNativeDirectory', - full_name='android.bundle.TargetedNativeDirectory', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='android.bundle.TargetedNativeDirectory.path', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='targeting', full_name='android.bundle.TargetedNativeDirectory.targeting', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=369, - serialized_end=469, -) - - -_TARGETEDAPEXIMAGE = _descriptor.Descriptor( - name='TargetedApexImage', - full_name='android.bundle.TargetedApexImage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='android.bundle.TargetedApexImage.path', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='build_info_path', full_name='android.bundle.TargetedApexImage.build_info_path', index=1, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='targeting', full_name='android.bundle.TargetedApexImage.targeting', index=2, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=471, - serialized_end=584, -) - -_ASSETS.fields_by_name['directory'].message_type = _TARGETEDASSETSDIRECTORY -_NATIVELIBRARIES.fields_by_name['directory'].message_type = _TARGETEDNATIVEDIRECTORY -_APEXIMAGES.fields_by_name['image'].message_type = _TARGETEDAPEXIMAGE -_TARGETEDASSETSDIRECTORY.fields_by_name['targeting'].message_type = targeting__pb2._ASSETSDIRECTORYTARGETING -_TARGETEDNATIVEDIRECTORY.fields_by_name['targeting'].message_type = targeting__pb2._NATIVEDIRECTORYTARGETING -_TARGETEDAPEXIMAGE.fields_by_name['targeting'].message_type = targeting__pb2._APEXIMAGETARGETING -DESCRIPTOR.message_types_by_name['Assets'] = _ASSETS -DESCRIPTOR.message_types_by_name['NativeLibraries'] = _NATIVELIBRARIES -DESCRIPTOR.message_types_by_name['ApexImages'] = _APEXIMAGES -DESCRIPTOR.message_types_by_name['TargetedAssetsDirectory'] = _TARGETEDASSETSDIRECTORY -DESCRIPTOR.message_types_by_name['TargetedNativeDirectory'] = _TARGETEDNATIVEDIRECTORY -DESCRIPTOR.message_types_by_name['TargetedApexImage'] = _TARGETEDAPEXIMAGE -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Assets = _reflection.GeneratedProtocolMessageType('Assets', (_message.Message,), { - 'DESCRIPTOR' : _ASSETS, - '__module__' : 'files_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.Assets) - }) -_sym_db.RegisterMessage(Assets) - -NativeLibraries = _reflection.GeneratedProtocolMessageType('NativeLibraries', (_message.Message,), { - 'DESCRIPTOR' : _NATIVELIBRARIES, - '__module__' : 'files_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.NativeLibraries) - }) -_sym_db.RegisterMessage(NativeLibraries) - -ApexImages = _reflection.GeneratedProtocolMessageType('ApexImages', (_message.Message,), { - 'DESCRIPTOR' : _APEXIMAGES, - '__module__' : 'files_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.ApexImages) - }) -_sym_db.RegisterMessage(ApexImages) - -TargetedAssetsDirectory = _reflection.GeneratedProtocolMessageType('TargetedAssetsDirectory', (_message.Message,), { - 'DESCRIPTOR' : _TARGETEDASSETSDIRECTORY, - '__module__' : 'files_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.TargetedAssetsDirectory) - }) -_sym_db.RegisterMessage(TargetedAssetsDirectory) - -TargetedNativeDirectory = _reflection.GeneratedProtocolMessageType('TargetedNativeDirectory', (_message.Message,), { - 'DESCRIPTOR' : _TARGETEDNATIVEDIRECTORY, - '__module__' : 'files_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.TargetedNativeDirectory) - }) -_sym_db.RegisterMessage(TargetedNativeDirectory) - -TargetedApexImage = _reflection.GeneratedProtocolMessageType('TargetedApexImage', (_message.Message,), { - 'DESCRIPTOR' : _TARGETEDAPEXIMAGE, - '__module__' : 'files_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.TargetedApexImage) - }) -_sym_db.RegisterMessage(TargetedApexImage) - - -DESCRIPTOR._options = None +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'files_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\022com.android.bundle' + _globals['_ASSETS']._serialized_start=48 + _globals['_ASSETS']._serialized_end=116 + _globals['_NATIVELIBRARIES']._serialized_start=118 + _globals['_NATIVELIBRARIES']._serialized_end=195 + _globals['_APEXIMAGES']._serialized_start=197 + _globals['_APEXIMAGES']._serialized_end=265 + _globals['_TARGETEDASSETSDIRECTORY']._serialized_start=267 + _globals['_TARGETEDASSETSDIRECTORY']._serialized_end=367 + _globals['_TARGETEDNATIVEDIRECTORY']._serialized_start=369 + _globals['_TARGETEDNATIVEDIRECTORY']._serialized_end=469 + _globals['_TARGETEDAPEXIMAGE']._serialized_start=471 + _globals['_TARGETEDAPEXIMAGE']._serialized_end=584 # @@protoc_insertion_point(module_scope) diff --git a/direct/src/dist/_proto/targeting_pb2.py b/direct/src/dist/_proto/targeting_pb2.py index b475f55af4..895eb1d1da 100644 --- a/direct/src/dist/_proto/targeting_pb2.py +++ b/direct/src/dist/_proto/targeting_pb2.py @@ -1,11 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: targeting.proto +# Protobuf Python Version: 6.33.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'targeting.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -14,1457 +25,84 @@ _sym_db = _symbol_database.Default() from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 -DESCRIPTOR = _descriptor.FileDescriptor( - name='targeting.proto', - package='android.bundle', - syntax='proto3', - serialized_options=b'\n\022com.android.bundle', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x0ftargeting.proto\x12\x0e\x61ndroid.bundle\x1a\x1egoogle/protobuf/wrappers.proto\"\xf6\x02\n\x10VariantTargeting\x12\x42\n\x15sdk_version_targeting\x18\x01 \x01(\x0b\x32#.android.bundle.SdkVersionTargeting\x12\x33\n\rabi_targeting\x18\x02 \x01(\x0b\x32\x1c.android.bundle.AbiTargeting\x12H\n\x18screen_density_targeting\x18\x03 \x01(\x0b\x32&.android.bundle.ScreenDensityTargeting\x12>\n\x13multi_abi_targeting\x18\x04 \x01(\x0b\x32!.android.bundle.MultiAbiTargeting\x12_\n$texture_compression_format_targeting\x18\x05 \x01(\x0b\x32\x31.android.bundle.TextureCompressionFormatTargeting\"\xbc\x04\n\x0c\x41pkTargeting\x12\x33\n\rabi_targeting\x18\x01 \x01(\x0b\x32\x1c.android.bundle.AbiTargeting\x12=\n\x12language_targeting\x18\x03 \x01(\x0b\x32!.android.bundle.LanguageTargeting\x12H\n\x18screen_density_targeting\x18\x04 \x01(\x0b\x32&.android.bundle.ScreenDensityTargeting\x12\x42\n\x15sdk_version_targeting\x18\x05 \x01(\x0b\x32#.android.bundle.SdkVersionTargeting\x12_\n$texture_compression_format_targeting\x18\x06 \x01(\x0b\x32\x31.android.bundle.TextureCompressionFormatTargeting\x12>\n\x13multi_abi_targeting\x18\x07 \x01(\x0b\x32!.android.bundle.MultiAbiTargeting\x12?\n\x13sanitizer_targeting\x18\x08 \x01(\x0b\x32\".android.bundle.SanitizerTargeting\x12\x42\n\x15\x64\x65vice_tier_targeting\x18\t \x01(\x0b\x32#.android.bundle.DeviceTierTargetingJ\x04\x08\x02\x10\x03\"\xbb\x02\n\x0fModuleTargeting\x12\x42\n\x15sdk_version_targeting\x18\x01 \x01(\x0b\x32#.android.bundle.SdkVersionTargeting\x12H\n\x18\x64\x65vice_feature_targeting\x18\x02 \x03(\x0b\x32&.android.bundle.DeviceFeatureTargeting\x12H\n\x18user_countries_targeting\x18\x03 \x01(\x0b\x32&.android.bundle.UserCountriesTargeting\x12J\n\x16\x64\x65vice_group_targeting\x18\x05 \x01(\x0b\x32*.android.bundle.DeviceGroupModuleTargetingJ\x04\x08\x04\x10\x05\"@\n\x16UserCountriesTargeting\x12\x15\n\rcountry_codes\x18\x01 \x03(\t\x12\x0f\n\x07\x65xclude\x18\x02 \x01(\x08\"\xfd\x01\n\rScreenDensity\x12\x43\n\rdensity_alias\x18\x01 \x01(\x0e\x32*.android.bundle.ScreenDensity.DensityAliasH\x00\x12\x15\n\x0b\x64\x65nsity_dpi\x18\x02 \x01(\x05H\x00\"\x7f\n\x0c\x44\x65nsityAlias\x12\x17\n\x13\x44\x45NSITY_UNSPECIFIED\x10\x00\x12\t\n\x05NODPI\x10\x01\x12\x08\n\x04LDPI\x10\x02\x12\x08\n\x04MDPI\x10\x03\x12\t\n\x05TVDPI\x10\x04\x12\x08\n\x04HDPI\x10\x05\x12\t\n\x05XHDPI\x10\x06\x12\n\n\x06XXHDPI\x10\x07\x12\x0b\n\x07XXXHDPI\x10\x08\x42\x0f\n\rdensity_oneof\"6\n\nSdkVersion\x12(\n\x03min\x18\x01 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\"\xb0\x02\n\x18TextureCompressionFormat\x12U\n\x05\x61lias\x18\x01 \x01(\x0e\x32\x46.android.bundle.TextureCompressionFormat.TextureCompressionFormatAlias\"\xbc\x01\n\x1dTextureCompressionFormatAlias\x12*\n&UNSPECIFIED_TEXTURE_COMPRESSION_FORMAT\x10\x00\x12\r\n\tETC1_RGB8\x10\x01\x12\x0c\n\x08PALETTED\x10\x02\x12\x0c\n\x08THREE_DC\x10\x03\x12\x07\n\x03\x41TC\x10\x04\x12\x08\n\x04LATC\x10\x05\x12\x08\n\x04\x44XT1\x10\x06\x12\x08\n\x04S3TC\x10\x07\x12\t\n\x05PVRTC\x10\x08\x12\x08\n\x04\x41STC\x10\t\x12\x08\n\x04\x45TC2\x10\n\"\xb9\x01\n\x03\x41\x62i\x12+\n\x05\x61lias\x18\x01 \x01(\x0e\x32\x1c.android.bundle.Abi.AbiAlias\"\x84\x01\n\x08\x41\x62iAlias\x12 \n\x1cUNSPECIFIED_CPU_ARCHITECTURE\x10\x00\x12\x0b\n\x07\x41RMEABI\x10\x01\x12\x0f\n\x0b\x41RMEABI_V7A\x10\x02\x12\r\n\tARM64_V8A\x10\x03\x12\x07\n\x03X86\x10\x04\x12\n\n\x06X86_64\x10\x05\x12\x08\n\x04MIPS\x10\x06\x12\n\n\x06MIPS64\x10\x07\",\n\x08MultiAbi\x12 \n\x03\x61\x62i\x18\x01 \x03(\x0b\x32\x13.android.bundle.Abi\"o\n\tSanitizer\x12\x37\n\x05\x61lias\x18\x01 \x01(\x0e\x32(.android.bundle.Sanitizer.SanitizerAlias\")\n\x0eSanitizerAlias\x12\x08\n\x04NONE\x10\x00\x12\r\n\tHWADDRESS\x10\x01\">\n\rDeviceFeature\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x01 \x01(\t\x12\x17\n\x0f\x66\x65\x61ture_version\x18\x02 \x01(\x05\"\x91\x02\n\x18\x41ssetsDirectoryTargeting\x12)\n\x03\x61\x62i\x18\x01 \x01(\x0b\x32\x1c.android.bundle.AbiTargeting\x12U\n\x1atexture_compression_format\x18\x03 \x01(\x0b\x32\x31.android.bundle.TextureCompressionFormatTargeting\x12\x33\n\x08language\x18\x04 \x01(\x0b\x32!.android.bundle.LanguageTargeting\x12\x38\n\x0b\x64\x65vice_tier\x18\x05 \x01(\x0b\x32#.android.bundle.DeviceTierTargetingJ\x04\x08\x02\x10\x03\"\xbe\x01\n\x18NativeDirectoryTargeting\x12 \n\x03\x61\x62i\x18\x01 \x01(\x0b\x32\x13.android.bundle.Abi\x12L\n\x1atexture_compression_format\x18\x03 \x01(\x0b\x32(.android.bundle.TextureCompressionFormat\x12,\n\tsanitizer\x18\x04 \x01(\x0b\x32\x19.android.bundle.SanitizerJ\x04\x08\x02\x10\x03\"J\n\x12\x41pexImageTargeting\x12\x34\n\tmulti_abi\x18\x01 \x01(\x0b\x32!.android.bundle.MultiAbiTargeting\"]\n\x0c\x41\x62iTargeting\x12\"\n\x05value\x18\x01 \x03(\x0b\x32\x13.android.bundle.Abi\x12)\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32\x13.android.bundle.Abi\"l\n\x11MultiAbiTargeting\x12\'\n\x05value\x18\x01 \x03(\x0b\x32\x18.android.bundle.MultiAbi\x12.\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32\x18.android.bundle.MultiAbi\"{\n\x16ScreenDensityTargeting\x12,\n\x05value\x18\x01 \x03(\x0b\x32\x1d.android.bundle.ScreenDensity\x12\x33\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32\x1d.android.bundle.ScreenDensity\"8\n\x11LanguageTargeting\x12\r\n\x05value\x18\x01 \x03(\t\x12\x14\n\x0c\x61lternatives\x18\x02 \x03(\t\"r\n\x13SdkVersionTargeting\x12)\n\x05value\x18\x01 \x03(\x0b\x32\x1a.android.bundle.SdkVersion\x12\x30\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32\x1a.android.bundle.SdkVersion\"\x9c\x01\n!TextureCompressionFormatTargeting\x12\x37\n\x05value\x18\x01 \x03(\x0b\x32(.android.bundle.TextureCompressionFormat\x12>\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32(.android.bundle.TextureCompressionFormat\">\n\x12SanitizerTargeting\x12(\n\x05value\x18\x01 \x03(\x0b\x32\x19.android.bundle.Sanitizer\"Q\n\x16\x44\x65viceFeatureTargeting\x12\x37\n\x10required_feature\x18\x01 \x01(\x0b\x32\x1d.android.bundle.DeviceFeature\"\x80\x01\n\x13\x44\x65viceTierTargeting\x12*\n\x05value\x18\x03 \x03(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x31\n\x0c\x61lternatives\x18\x04 \x03(\x0b\x32\x1b.google.protobuf.Int32ValueJ\x04\x08\x01\x10\x02J\x04\x08\x02\x10\x03\"+\n\x1a\x44\x65viceGroupModuleTargeting\x12\r\n\x05value\x18\x01 \x03(\tB\x14\n\x12\x63om.android.bundleb\x06proto3' - , - dependencies=[google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,]) - - - -_SCREENDENSITY_DENSITYALIAS = _descriptor.EnumDescriptor( - name='DensityAlias', - full_name='android.bundle.ScreenDensity.DensityAlias', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='DENSITY_UNSPECIFIED', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NODPI', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='LDPI', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='MDPI', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TVDPI', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='HDPI', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='XHDPI', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='XXHDPI', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='XXXHDPI', index=8, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1513, - serialized_end=1640, -) -_sym_db.RegisterEnumDescriptor(_SCREENDENSITY_DENSITYALIAS) - -_TEXTURECOMPRESSIONFORMAT_TEXTURECOMPRESSIONFORMATALIAS = _descriptor.EnumDescriptor( - name='TextureCompressionFormatAlias', - full_name='android.bundle.TextureCompressionFormat.TextureCompressionFormatAlias', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNSPECIFIED_TEXTURE_COMPRESSION_FORMAT', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ETC1_RGB8', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PALETTED', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='THREE_DC', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ATC', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='LATC', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='DXT1', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='S3TC', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='PVRTC', index=8, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ASTC', index=9, number=9, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ETC2', index=10, number=10, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=1832, - serialized_end=2020, -) -_sym_db.RegisterEnumDescriptor(_TEXTURECOMPRESSIONFORMAT_TEXTURECOMPRESSIONFORMATALIAS) - -_ABI_ABIALIAS = _descriptor.EnumDescriptor( - name='AbiAlias', - full_name='android.bundle.Abi.AbiAlias', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNSPECIFIED_CPU_ARCHITECTURE', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ARMEABI', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ARMEABI_V7A', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ARM64_V8A', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='X86', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='X86_64', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='MIPS', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='MIPS64', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2076, - serialized_end=2208, -) -_sym_db.RegisterEnumDescriptor(_ABI_ABIALIAS) - -_SANITIZER_SANITIZERALIAS = _descriptor.EnumDescriptor( - name='SanitizerAlias', - full_name='android.bundle.Sanitizer.SanitizerAlias', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='NONE', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='HWADDRESS', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=2326, - serialized_end=2367, -) -_sym_db.RegisterEnumDescriptor(_SANITIZER_SANITIZERALIAS) - - -_VARIANTTARGETING = _descriptor.Descriptor( - name='VariantTargeting', - full_name='android.bundle.VariantTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='sdk_version_targeting', full_name='android.bundle.VariantTargeting.sdk_version_targeting', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='abi_targeting', full_name='android.bundle.VariantTargeting.abi_targeting', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='screen_density_targeting', full_name='android.bundle.VariantTargeting.screen_density_targeting', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='multi_abi_targeting', full_name='android.bundle.VariantTargeting.multi_abi_targeting', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='texture_compression_format_targeting', full_name='android.bundle.VariantTargeting.texture_compression_format_targeting', index=4, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=68, - serialized_end=442, -) - - -_APKTARGETING = _descriptor.Descriptor( - name='ApkTargeting', - full_name='android.bundle.ApkTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='abi_targeting', full_name='android.bundle.ApkTargeting.abi_targeting', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='language_targeting', full_name='android.bundle.ApkTargeting.language_targeting', index=1, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='screen_density_targeting', full_name='android.bundle.ApkTargeting.screen_density_targeting', index=2, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='sdk_version_targeting', full_name='android.bundle.ApkTargeting.sdk_version_targeting', index=3, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='texture_compression_format_targeting', full_name='android.bundle.ApkTargeting.texture_compression_format_targeting', index=4, - number=6, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='multi_abi_targeting', full_name='android.bundle.ApkTargeting.multi_abi_targeting', index=5, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='sanitizer_targeting', full_name='android.bundle.ApkTargeting.sanitizer_targeting', index=6, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='device_tier_targeting', full_name='android.bundle.ApkTargeting.device_tier_targeting', index=7, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=445, - serialized_end=1017, -) - - -_MODULETARGETING = _descriptor.Descriptor( - name='ModuleTargeting', - full_name='android.bundle.ModuleTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='sdk_version_targeting', full_name='android.bundle.ModuleTargeting.sdk_version_targeting', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='device_feature_targeting', full_name='android.bundle.ModuleTargeting.device_feature_targeting', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='user_countries_targeting', full_name='android.bundle.ModuleTargeting.user_countries_targeting', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='device_group_targeting', full_name='android.bundle.ModuleTargeting.device_group_targeting', index=3, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1020, - serialized_end=1335, -) - - -_USERCOUNTRIESTARGETING = _descriptor.Descriptor( - name='UserCountriesTargeting', - full_name='android.bundle.UserCountriesTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='country_codes', full_name='android.bundle.UserCountriesTargeting.country_codes', index=0, - number=1, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='exclude', full_name='android.bundle.UserCountriesTargeting.exclude', index=1, - number=2, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1337, - serialized_end=1401, -) - - -_SCREENDENSITY = _descriptor.Descriptor( - name='ScreenDensity', - full_name='android.bundle.ScreenDensity', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='density_alias', full_name='android.bundle.ScreenDensity.density_alias', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='density_dpi', full_name='android.bundle.ScreenDensity.density_dpi', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _SCREENDENSITY_DENSITYALIAS, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='density_oneof', full_name='android.bundle.ScreenDensity.density_oneof', - index=0, containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[]), - ], - serialized_start=1404, - serialized_end=1657, -) - - -_SDKVERSION = _descriptor.Descriptor( - name='SdkVersion', - full_name='android.bundle.SdkVersion', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='min', full_name='android.bundle.SdkVersion.min', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1659, - serialized_end=1713, -) - - -_TEXTURECOMPRESSIONFORMAT = _descriptor.Descriptor( - name='TextureCompressionFormat', - full_name='android.bundle.TextureCompressionFormat', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='alias', full_name='android.bundle.TextureCompressionFormat.alias', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _TEXTURECOMPRESSIONFORMAT_TEXTURECOMPRESSIONFORMATALIAS, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1716, - serialized_end=2020, -) - - -_ABI = _descriptor.Descriptor( - name='Abi', - full_name='android.bundle.Abi', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='alias', full_name='android.bundle.Abi.alias', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _ABI_ABIALIAS, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2023, - serialized_end=2208, -) - - -_MULTIABI = _descriptor.Descriptor( - name='MultiAbi', - full_name='android.bundle.MultiAbi', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='abi', full_name='android.bundle.MultiAbi.abi', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2210, - serialized_end=2254, -) - - -_SANITIZER = _descriptor.Descriptor( - name='Sanitizer', - full_name='android.bundle.Sanitizer', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='alias', full_name='android.bundle.Sanitizer.alias', index=0, - number=1, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _SANITIZER_SANITIZERALIAS, - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2256, - serialized_end=2367, -) - - -_DEVICEFEATURE = _descriptor.Descriptor( - name='DeviceFeature', - full_name='android.bundle.DeviceFeature', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='feature_name', full_name='android.bundle.DeviceFeature.feature_name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='feature_version', full_name='android.bundle.DeviceFeature.feature_version', index=1, - number=2, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2369, - serialized_end=2431, -) - - -_ASSETSDIRECTORYTARGETING = _descriptor.Descriptor( - name='AssetsDirectoryTargeting', - full_name='android.bundle.AssetsDirectoryTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='abi', full_name='android.bundle.AssetsDirectoryTargeting.abi', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='texture_compression_format', full_name='android.bundle.AssetsDirectoryTargeting.texture_compression_format', index=1, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='language', full_name='android.bundle.AssetsDirectoryTargeting.language', index=2, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='device_tier', full_name='android.bundle.AssetsDirectoryTargeting.device_tier', index=3, - number=5, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2434, - serialized_end=2707, -) - - -_NATIVEDIRECTORYTARGETING = _descriptor.Descriptor( - name='NativeDirectoryTargeting', - full_name='android.bundle.NativeDirectoryTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='abi', full_name='android.bundle.NativeDirectoryTargeting.abi', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='texture_compression_format', full_name='android.bundle.NativeDirectoryTargeting.texture_compression_format', index=1, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='sanitizer', full_name='android.bundle.NativeDirectoryTargeting.sanitizer', index=2, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2710, - serialized_end=2900, -) - - -_APEXIMAGETARGETING = _descriptor.Descriptor( - name='ApexImageTargeting', - full_name='android.bundle.ApexImageTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='multi_abi', full_name='android.bundle.ApexImageTargeting.multi_abi', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2902, - serialized_end=2976, -) - - -_ABITARGETING = _descriptor.Descriptor( - name='AbiTargeting', - full_name='android.bundle.AbiTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.AbiTargeting.value', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='alternatives', full_name='android.bundle.AbiTargeting.alternatives', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2978, - serialized_end=3071, -) - - -_MULTIABITARGETING = _descriptor.Descriptor( - name='MultiAbiTargeting', - full_name='android.bundle.MultiAbiTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.MultiAbiTargeting.value', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='alternatives', full_name='android.bundle.MultiAbiTargeting.alternatives', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3073, - serialized_end=3181, -) - - -_SCREENDENSITYTARGETING = _descriptor.Descriptor( - name='ScreenDensityTargeting', - full_name='android.bundle.ScreenDensityTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.ScreenDensityTargeting.value', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='alternatives', full_name='android.bundle.ScreenDensityTargeting.alternatives', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3183, - serialized_end=3306, -) - - -_LANGUAGETARGETING = _descriptor.Descriptor( - name='LanguageTargeting', - full_name='android.bundle.LanguageTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.LanguageTargeting.value', index=0, - number=1, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='alternatives', full_name='android.bundle.LanguageTargeting.alternatives', index=1, - number=2, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3308, - serialized_end=3364, -) - - -_SDKVERSIONTARGETING = _descriptor.Descriptor( - name='SdkVersionTargeting', - full_name='android.bundle.SdkVersionTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.SdkVersionTargeting.value', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='alternatives', full_name='android.bundle.SdkVersionTargeting.alternatives', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3366, - serialized_end=3480, -) - - -_TEXTURECOMPRESSIONFORMATTARGETING = _descriptor.Descriptor( - name='TextureCompressionFormatTargeting', - full_name='android.bundle.TextureCompressionFormatTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.TextureCompressionFormatTargeting.value', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='alternatives', full_name='android.bundle.TextureCompressionFormatTargeting.alternatives', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3483, - serialized_end=3639, -) - - -_SANITIZERTARGETING = _descriptor.Descriptor( - name='SanitizerTargeting', - full_name='android.bundle.SanitizerTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.SanitizerTargeting.value', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3641, - serialized_end=3703, -) - - -_DEVICEFEATURETARGETING = _descriptor.Descriptor( - name='DeviceFeatureTargeting', - full_name='android.bundle.DeviceFeatureTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='required_feature', full_name='android.bundle.DeviceFeatureTargeting.required_feature', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3705, - serialized_end=3786, -) - - -_DEVICETIERTARGETING = _descriptor.Descriptor( - name='DeviceTierTargeting', - full_name='android.bundle.DeviceTierTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.DeviceTierTargeting.value', index=0, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='alternatives', full_name='android.bundle.DeviceTierTargeting.alternatives', index=1, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3789, - serialized_end=3917, -) - - -_DEVICEGROUPMODULETARGETING = _descriptor.Descriptor( - name='DeviceGroupModuleTargeting', - full_name='android.bundle.DeviceGroupModuleTargeting', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='android.bundle.DeviceGroupModuleTargeting.value', index=0, - number=1, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=3919, - serialized_end=3962, -) - -_VARIANTTARGETING.fields_by_name['sdk_version_targeting'].message_type = _SDKVERSIONTARGETING -_VARIANTTARGETING.fields_by_name['abi_targeting'].message_type = _ABITARGETING -_VARIANTTARGETING.fields_by_name['screen_density_targeting'].message_type = _SCREENDENSITYTARGETING -_VARIANTTARGETING.fields_by_name['multi_abi_targeting'].message_type = _MULTIABITARGETING -_VARIANTTARGETING.fields_by_name['texture_compression_format_targeting'].message_type = _TEXTURECOMPRESSIONFORMATTARGETING -_APKTARGETING.fields_by_name['abi_targeting'].message_type = _ABITARGETING -_APKTARGETING.fields_by_name['language_targeting'].message_type = _LANGUAGETARGETING -_APKTARGETING.fields_by_name['screen_density_targeting'].message_type = _SCREENDENSITYTARGETING -_APKTARGETING.fields_by_name['sdk_version_targeting'].message_type = _SDKVERSIONTARGETING -_APKTARGETING.fields_by_name['texture_compression_format_targeting'].message_type = _TEXTURECOMPRESSIONFORMATTARGETING -_APKTARGETING.fields_by_name['multi_abi_targeting'].message_type = _MULTIABITARGETING -_APKTARGETING.fields_by_name['sanitizer_targeting'].message_type = _SANITIZERTARGETING -_APKTARGETING.fields_by_name['device_tier_targeting'].message_type = _DEVICETIERTARGETING -_MODULETARGETING.fields_by_name['sdk_version_targeting'].message_type = _SDKVERSIONTARGETING -_MODULETARGETING.fields_by_name['device_feature_targeting'].message_type = _DEVICEFEATURETARGETING -_MODULETARGETING.fields_by_name['user_countries_targeting'].message_type = _USERCOUNTRIESTARGETING -_MODULETARGETING.fields_by_name['device_group_targeting'].message_type = _DEVICEGROUPMODULETARGETING -_SCREENDENSITY.fields_by_name['density_alias'].enum_type = _SCREENDENSITY_DENSITYALIAS -_SCREENDENSITY_DENSITYALIAS.containing_type = _SCREENDENSITY -_SCREENDENSITY.oneofs_by_name['density_oneof'].fields.append( - _SCREENDENSITY.fields_by_name['density_alias']) -_SCREENDENSITY.fields_by_name['density_alias'].containing_oneof = _SCREENDENSITY.oneofs_by_name['density_oneof'] -_SCREENDENSITY.oneofs_by_name['density_oneof'].fields.append( - _SCREENDENSITY.fields_by_name['density_dpi']) -_SCREENDENSITY.fields_by_name['density_dpi'].containing_oneof = _SCREENDENSITY.oneofs_by_name['density_oneof'] -_SDKVERSION.fields_by_name['min'].message_type = google_dot_protobuf_dot_wrappers__pb2._INT32VALUE -_TEXTURECOMPRESSIONFORMAT.fields_by_name['alias'].enum_type = _TEXTURECOMPRESSIONFORMAT_TEXTURECOMPRESSIONFORMATALIAS -_TEXTURECOMPRESSIONFORMAT_TEXTURECOMPRESSIONFORMATALIAS.containing_type = _TEXTURECOMPRESSIONFORMAT -_ABI.fields_by_name['alias'].enum_type = _ABI_ABIALIAS -_ABI_ABIALIAS.containing_type = _ABI -_MULTIABI.fields_by_name['abi'].message_type = _ABI -_SANITIZER.fields_by_name['alias'].enum_type = _SANITIZER_SANITIZERALIAS -_SANITIZER_SANITIZERALIAS.containing_type = _SANITIZER -_ASSETSDIRECTORYTARGETING.fields_by_name['abi'].message_type = _ABITARGETING -_ASSETSDIRECTORYTARGETING.fields_by_name['texture_compression_format'].message_type = _TEXTURECOMPRESSIONFORMATTARGETING -_ASSETSDIRECTORYTARGETING.fields_by_name['language'].message_type = _LANGUAGETARGETING -_ASSETSDIRECTORYTARGETING.fields_by_name['device_tier'].message_type = _DEVICETIERTARGETING -_NATIVEDIRECTORYTARGETING.fields_by_name['abi'].message_type = _ABI -_NATIVEDIRECTORYTARGETING.fields_by_name['texture_compression_format'].message_type = _TEXTURECOMPRESSIONFORMAT -_NATIVEDIRECTORYTARGETING.fields_by_name['sanitizer'].message_type = _SANITIZER -_APEXIMAGETARGETING.fields_by_name['multi_abi'].message_type = _MULTIABITARGETING -_ABITARGETING.fields_by_name['value'].message_type = _ABI -_ABITARGETING.fields_by_name['alternatives'].message_type = _ABI -_MULTIABITARGETING.fields_by_name['value'].message_type = _MULTIABI -_MULTIABITARGETING.fields_by_name['alternatives'].message_type = _MULTIABI -_SCREENDENSITYTARGETING.fields_by_name['value'].message_type = _SCREENDENSITY -_SCREENDENSITYTARGETING.fields_by_name['alternatives'].message_type = _SCREENDENSITY -_SDKVERSIONTARGETING.fields_by_name['value'].message_type = _SDKVERSION -_SDKVERSIONTARGETING.fields_by_name['alternatives'].message_type = _SDKVERSION -_TEXTURECOMPRESSIONFORMATTARGETING.fields_by_name['value'].message_type = _TEXTURECOMPRESSIONFORMAT -_TEXTURECOMPRESSIONFORMATTARGETING.fields_by_name['alternatives'].message_type = _TEXTURECOMPRESSIONFORMAT -_SANITIZERTARGETING.fields_by_name['value'].message_type = _SANITIZER -_DEVICEFEATURETARGETING.fields_by_name['required_feature'].message_type = _DEVICEFEATURE -_DEVICETIERTARGETING.fields_by_name['value'].message_type = google_dot_protobuf_dot_wrappers__pb2._INT32VALUE -_DEVICETIERTARGETING.fields_by_name['alternatives'].message_type = google_dot_protobuf_dot_wrappers__pb2._INT32VALUE -DESCRIPTOR.message_types_by_name['VariantTargeting'] = _VARIANTTARGETING -DESCRIPTOR.message_types_by_name['ApkTargeting'] = _APKTARGETING -DESCRIPTOR.message_types_by_name['ModuleTargeting'] = _MODULETARGETING -DESCRIPTOR.message_types_by_name['UserCountriesTargeting'] = _USERCOUNTRIESTARGETING -DESCRIPTOR.message_types_by_name['ScreenDensity'] = _SCREENDENSITY -DESCRIPTOR.message_types_by_name['SdkVersion'] = _SDKVERSION -DESCRIPTOR.message_types_by_name['TextureCompressionFormat'] = _TEXTURECOMPRESSIONFORMAT -DESCRIPTOR.message_types_by_name['Abi'] = _ABI -DESCRIPTOR.message_types_by_name['MultiAbi'] = _MULTIABI -DESCRIPTOR.message_types_by_name['Sanitizer'] = _SANITIZER -DESCRIPTOR.message_types_by_name['DeviceFeature'] = _DEVICEFEATURE -DESCRIPTOR.message_types_by_name['AssetsDirectoryTargeting'] = _ASSETSDIRECTORYTARGETING -DESCRIPTOR.message_types_by_name['NativeDirectoryTargeting'] = _NATIVEDIRECTORYTARGETING -DESCRIPTOR.message_types_by_name['ApexImageTargeting'] = _APEXIMAGETARGETING -DESCRIPTOR.message_types_by_name['AbiTargeting'] = _ABITARGETING -DESCRIPTOR.message_types_by_name['MultiAbiTargeting'] = _MULTIABITARGETING -DESCRIPTOR.message_types_by_name['ScreenDensityTargeting'] = _SCREENDENSITYTARGETING -DESCRIPTOR.message_types_by_name['LanguageTargeting'] = _LANGUAGETARGETING -DESCRIPTOR.message_types_by_name['SdkVersionTargeting'] = _SDKVERSIONTARGETING -DESCRIPTOR.message_types_by_name['TextureCompressionFormatTargeting'] = _TEXTURECOMPRESSIONFORMATTARGETING -DESCRIPTOR.message_types_by_name['SanitizerTargeting'] = _SANITIZERTARGETING -DESCRIPTOR.message_types_by_name['DeviceFeatureTargeting'] = _DEVICEFEATURETARGETING -DESCRIPTOR.message_types_by_name['DeviceTierTargeting'] = _DEVICETIERTARGETING -DESCRIPTOR.message_types_by_name['DeviceGroupModuleTargeting'] = _DEVICEGROUPMODULETARGETING -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -VariantTargeting = _reflection.GeneratedProtocolMessageType('VariantTargeting', (_message.Message,), { - 'DESCRIPTOR' : _VARIANTTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.VariantTargeting) - }) -_sym_db.RegisterMessage(VariantTargeting) - -ApkTargeting = _reflection.GeneratedProtocolMessageType('ApkTargeting', (_message.Message,), { - 'DESCRIPTOR' : _APKTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.ApkTargeting) - }) -_sym_db.RegisterMessage(ApkTargeting) - -ModuleTargeting = _reflection.GeneratedProtocolMessageType('ModuleTargeting', (_message.Message,), { - 'DESCRIPTOR' : _MODULETARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.ModuleTargeting) - }) -_sym_db.RegisterMessage(ModuleTargeting) - -UserCountriesTargeting = _reflection.GeneratedProtocolMessageType('UserCountriesTargeting', (_message.Message,), { - 'DESCRIPTOR' : _USERCOUNTRIESTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.UserCountriesTargeting) - }) -_sym_db.RegisterMessage(UserCountriesTargeting) - -ScreenDensity = _reflection.GeneratedProtocolMessageType('ScreenDensity', (_message.Message,), { - 'DESCRIPTOR' : _SCREENDENSITY, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.ScreenDensity) - }) -_sym_db.RegisterMessage(ScreenDensity) - -SdkVersion = _reflection.GeneratedProtocolMessageType('SdkVersion', (_message.Message,), { - 'DESCRIPTOR' : _SDKVERSION, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.SdkVersion) - }) -_sym_db.RegisterMessage(SdkVersion) - -TextureCompressionFormat = _reflection.GeneratedProtocolMessageType('TextureCompressionFormat', (_message.Message,), { - 'DESCRIPTOR' : _TEXTURECOMPRESSIONFORMAT, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.TextureCompressionFormat) - }) -_sym_db.RegisterMessage(TextureCompressionFormat) - -Abi = _reflection.GeneratedProtocolMessageType('Abi', (_message.Message,), { - 'DESCRIPTOR' : _ABI, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.Abi) - }) -_sym_db.RegisterMessage(Abi) - -MultiAbi = _reflection.GeneratedProtocolMessageType('MultiAbi', (_message.Message,), { - 'DESCRIPTOR' : _MULTIABI, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.MultiAbi) - }) -_sym_db.RegisterMessage(MultiAbi) - -Sanitizer = _reflection.GeneratedProtocolMessageType('Sanitizer', (_message.Message,), { - 'DESCRIPTOR' : _SANITIZER, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.Sanitizer) - }) -_sym_db.RegisterMessage(Sanitizer) - -DeviceFeature = _reflection.GeneratedProtocolMessageType('DeviceFeature', (_message.Message,), { - 'DESCRIPTOR' : _DEVICEFEATURE, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.DeviceFeature) - }) -_sym_db.RegisterMessage(DeviceFeature) - -AssetsDirectoryTargeting = _reflection.GeneratedProtocolMessageType('AssetsDirectoryTargeting', (_message.Message,), { - 'DESCRIPTOR' : _ASSETSDIRECTORYTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.AssetsDirectoryTargeting) - }) -_sym_db.RegisterMessage(AssetsDirectoryTargeting) - -NativeDirectoryTargeting = _reflection.GeneratedProtocolMessageType('NativeDirectoryTargeting', (_message.Message,), { - 'DESCRIPTOR' : _NATIVEDIRECTORYTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.NativeDirectoryTargeting) - }) -_sym_db.RegisterMessage(NativeDirectoryTargeting) - -ApexImageTargeting = _reflection.GeneratedProtocolMessageType('ApexImageTargeting', (_message.Message,), { - 'DESCRIPTOR' : _APEXIMAGETARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.ApexImageTargeting) - }) -_sym_db.RegisterMessage(ApexImageTargeting) - -AbiTargeting = _reflection.GeneratedProtocolMessageType('AbiTargeting', (_message.Message,), { - 'DESCRIPTOR' : _ABITARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.AbiTargeting) - }) -_sym_db.RegisterMessage(AbiTargeting) - -MultiAbiTargeting = _reflection.GeneratedProtocolMessageType('MultiAbiTargeting', (_message.Message,), { - 'DESCRIPTOR' : _MULTIABITARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.MultiAbiTargeting) - }) -_sym_db.RegisterMessage(MultiAbiTargeting) - -ScreenDensityTargeting = _reflection.GeneratedProtocolMessageType('ScreenDensityTargeting', (_message.Message,), { - 'DESCRIPTOR' : _SCREENDENSITYTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.ScreenDensityTargeting) - }) -_sym_db.RegisterMessage(ScreenDensityTargeting) - -LanguageTargeting = _reflection.GeneratedProtocolMessageType('LanguageTargeting', (_message.Message,), { - 'DESCRIPTOR' : _LANGUAGETARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.LanguageTargeting) - }) -_sym_db.RegisterMessage(LanguageTargeting) - -SdkVersionTargeting = _reflection.GeneratedProtocolMessageType('SdkVersionTargeting', (_message.Message,), { - 'DESCRIPTOR' : _SDKVERSIONTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.SdkVersionTargeting) - }) -_sym_db.RegisterMessage(SdkVersionTargeting) - -TextureCompressionFormatTargeting = _reflection.GeneratedProtocolMessageType('TextureCompressionFormatTargeting', (_message.Message,), { - 'DESCRIPTOR' : _TEXTURECOMPRESSIONFORMATTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.TextureCompressionFormatTargeting) - }) -_sym_db.RegisterMessage(TextureCompressionFormatTargeting) - -SanitizerTargeting = _reflection.GeneratedProtocolMessageType('SanitizerTargeting', (_message.Message,), { - 'DESCRIPTOR' : _SANITIZERTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.SanitizerTargeting) - }) -_sym_db.RegisterMessage(SanitizerTargeting) - -DeviceFeatureTargeting = _reflection.GeneratedProtocolMessageType('DeviceFeatureTargeting', (_message.Message,), { - 'DESCRIPTOR' : _DEVICEFEATURETARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.DeviceFeatureTargeting) - }) -_sym_db.RegisterMessage(DeviceFeatureTargeting) - -DeviceTierTargeting = _reflection.GeneratedProtocolMessageType('DeviceTierTargeting', (_message.Message,), { - 'DESCRIPTOR' : _DEVICETIERTARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.DeviceTierTargeting) - }) -_sym_db.RegisterMessage(DeviceTierTargeting) - -DeviceGroupModuleTargeting = _reflection.GeneratedProtocolMessageType('DeviceGroupModuleTargeting', (_message.Message,), { - 'DESCRIPTOR' : _DEVICEGROUPMODULETARGETING, - '__module__' : 'targeting_pb2' - # @@protoc_insertion_point(class_scope:android.bundle.DeviceGroupModuleTargeting) - }) -_sym_db.RegisterMessage(DeviceGroupModuleTargeting) - - -DESCRIPTOR._options = None +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ftargeting.proto\x12\x0e\x61ndroid.bundle\x1a\x1egoogle/protobuf/wrappers.proto\"\xba\x03\n\x10VariantTargeting\x12\x42\n\x15sdk_version_targeting\x18\x01 \x01(\x0b\x32#.android.bundle.SdkVersionTargeting\x12\x33\n\rabi_targeting\x18\x02 \x01(\x0b\x32\x1c.android.bundle.AbiTargeting\x12H\n\x18screen_density_targeting\x18\x03 \x01(\x0b\x32&.android.bundle.ScreenDensityTargeting\x12>\n\x13multi_abi_targeting\x18\x04 \x01(\x0b\x32!.android.bundle.MultiAbiTargeting\x12_\n$texture_compression_format_targeting\x18\x05 \x01(\x0b\x32\x31.android.bundle.TextureCompressionFormatTargeting\x12\x42\n\x15sdk_runtime_targeting\x18\x06 \x01(\x0b\x32#.android.bundle.SdkRuntimeTargeting\"\xce\x05\n\x0c\x41pkTargeting\x12\x33\n\rabi_targeting\x18\x01 \x01(\x0b\x32\x1c.android.bundle.AbiTargeting\x12=\n\x12language_targeting\x18\x03 \x01(\x0b\x32!.android.bundle.LanguageTargeting\x12H\n\x18screen_density_targeting\x18\x04 \x01(\x0b\x32&.android.bundle.ScreenDensityTargeting\x12\x42\n\x15sdk_version_targeting\x18\x05 \x01(\x0b\x32#.android.bundle.SdkVersionTargeting\x12_\n$texture_compression_format_targeting\x18\x06 \x01(\x0b\x32\x31.android.bundle.TextureCompressionFormatTargeting\x12>\n\x13multi_abi_targeting\x18\x07 \x01(\x0b\x32!.android.bundle.MultiAbiTargeting\x12?\n\x13sanitizer_targeting\x18\x08 \x01(\x0b\x32\".android.bundle.SanitizerTargeting\x12\x46\n\x15\x64\x65vice_tier_targeting\x18\t \x01(\x0b\x32#.android.bundle.DeviceTierTargetingB\x02\x18\x01\x12\x46\n\x15\x63ountry_set_targeting\x18\n \x01(\x0b\x32#.android.bundle.CountrySetTargetingB\x02\x18\x01\x12\x44\n\x16\x64\x65vice_group_targeting\x18\x0b \x01(\x0b\x32$.android.bundle.DeviceGroupTargetingJ\x04\x08\x02\x10\x03\"\xbb\x02\n\x0fModuleTargeting\x12\x42\n\x15sdk_version_targeting\x18\x01 \x01(\x0b\x32#.android.bundle.SdkVersionTargeting\x12H\n\x18\x64\x65vice_feature_targeting\x18\x02 \x03(\x0b\x32&.android.bundle.DeviceFeatureTargeting\x12H\n\x18user_countries_targeting\x18\x03 \x01(\x0b\x32&.android.bundle.UserCountriesTargeting\x12J\n\x16\x64\x65vice_group_targeting\x18\x05 \x01(\x0b\x32*.android.bundle.DeviceGroupModuleTargetingJ\x04\x08\x04\x10\x05\"\xac\x01\n\x14\x41ssetModuleTargeting\x12H\n\x18user_countries_targeting\x18\x01 \x01(\x0b\x32&.android.bundle.UserCountriesTargeting\x12J\n\x16\x64\x65vice_group_targeting\x18\x02 \x01(\x0b\x32*.android.bundle.DeviceGroupModuleTargeting\"@\n\x16UserCountriesTargeting\x12\x15\n\rcountry_codes\x18\x01 \x03(\t\x12\x0f\n\x07\x65xclude\x18\x02 \x01(\x08\"\xfd\x01\n\rScreenDensity\x12\x43\n\rdensity_alias\x18\x01 \x01(\x0e\x32*.android.bundle.ScreenDensity.DensityAliasH\x00\x12\x15\n\x0b\x64\x65nsity_dpi\x18\x02 \x01(\x05H\x00\"\x7f\n\x0c\x44\x65nsityAlias\x12\x17\n\x13\x44\x45NSITY_UNSPECIFIED\x10\x00\x12\t\n\x05NODPI\x10\x01\x12\x08\n\x04LDPI\x10\x02\x12\x08\n\x04MDPI\x10\x03\x12\t\n\x05TVDPI\x10\x04\x12\x08\n\x04HDPI\x10\x05\x12\t\n\x05XHDPI\x10\x06\x12\n\n\x06XXHDPI\x10\x07\x12\x0b\n\x07XXXHDPI\x10\x08\x42\x0f\n\rdensity_oneof\"6\n\nSdkVersion\x12(\n\x03min\x18\x01 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\"\xb0\x02\n\x18TextureCompressionFormat\x12U\n\x05\x61lias\x18\x01 \x01(\x0e\x32\x46.android.bundle.TextureCompressionFormat.TextureCompressionFormatAlias\"\xbc\x01\n\x1dTextureCompressionFormatAlias\x12*\n&UNSPECIFIED_TEXTURE_COMPRESSION_FORMAT\x10\x00\x12\r\n\tETC1_RGB8\x10\x01\x12\x0c\n\x08PALETTED\x10\x02\x12\x0c\n\x08THREE_DC\x10\x03\x12\x07\n\x03\x41TC\x10\x04\x12\x08\n\x04LATC\x10\x05\x12\x08\n\x04\x44XT1\x10\x06\x12\x08\n\x04S3TC\x10\x07\x12\t\n\x05PVRTC\x10\x08\x12\x08\n\x04\x41STC\x10\t\x12\x08\n\x04\x45TC2\x10\n\"\xc6\x01\n\x03\x41\x62i\x12+\n\x05\x61lias\x18\x01 \x01(\x0e\x32\x1c.android.bundle.Abi.AbiAlias\"\x91\x01\n\x08\x41\x62iAlias\x12 \n\x1cUNSPECIFIED_CPU_ARCHITECTURE\x10\x00\x12\x0b\n\x07\x41RMEABI\x10\x01\x12\x0f\n\x0b\x41RMEABI_V7A\x10\x02\x12\r\n\tARM64_V8A\x10\x03\x12\x07\n\x03X86\x10\x04\x12\n\n\x06X86_64\x10\x05\x12\x08\n\x04MIPS\x10\x06\x12\n\n\x06MIPS64\x10\x07\x12\x0b\n\x07RISCV64\x10\x08\",\n\x08MultiAbi\x12 \n\x03\x61\x62i\x18\x01 \x03(\x0b\x32\x13.android.bundle.Abi\"o\n\tSanitizer\x12\x37\n\x05\x61lias\x18\x01 \x01(\x0e\x32(.android.bundle.Sanitizer.SanitizerAlias\")\n\x0eSanitizerAlias\x12\x08\n\x04NONE\x10\x00\x12\r\n\tHWADDRESS\x10\x01\">\n\rDeviceFeature\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x01 \x01(\t\x12\x17\n\x0f\x66\x65\x61ture_version\x18\x02 \x01(\x05\"\x8f\x03\n\x18\x41ssetsDirectoryTargeting\x12)\n\x03\x61\x62i\x18\x01 \x01(\x0b\x32\x1c.android.bundle.AbiTargeting\x12U\n\x1atexture_compression_format\x18\x03 \x01(\x0b\x32\x31.android.bundle.TextureCompressionFormatTargeting\x12\x33\n\x08language\x18\x04 \x01(\x0b\x32!.android.bundle.LanguageTargeting\x12<\n\x0b\x64\x65vice_tier\x18\x05 \x01(\x0b\x32#.android.bundle.DeviceTierTargetingB\x02\x18\x01\x12<\n\x0b\x63ountry_set\x18\x06 \x01(\x0b\x32#.android.bundle.CountrySetTargetingB\x02\x18\x01\x12:\n\x0c\x64\x65vice_group\x18\x07 \x01(\x0b\x32$.android.bundle.DeviceGroupTargetingJ\x04\x08\x02\x10\x03\"\xbe\x01\n\x18NativeDirectoryTargeting\x12 \n\x03\x61\x62i\x18\x01 \x01(\x0b\x32\x13.android.bundle.Abi\x12L\n\x1atexture_compression_format\x18\x03 \x01(\x0b\x32(.android.bundle.TextureCompressionFormat\x12,\n\tsanitizer\x18\x04 \x01(\x0b\x32\x19.android.bundle.SanitizerJ\x04\x08\x02\x10\x03\"J\n\x12\x41pexImageTargeting\x12\x34\n\tmulti_abi\x18\x01 \x01(\x0b\x32!.android.bundle.MultiAbiTargeting\"]\n\x0c\x41\x62iTargeting\x12\"\n\x05value\x18\x01 \x03(\x0b\x32\x13.android.bundle.Abi\x12)\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32\x13.android.bundle.Abi\"l\n\x11MultiAbiTargeting\x12\'\n\x05value\x18\x01 \x03(\x0b\x32\x18.android.bundle.MultiAbi\x12.\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32\x18.android.bundle.MultiAbi\"{\n\x16ScreenDensityTargeting\x12,\n\x05value\x18\x01 \x03(\x0b\x32\x1d.android.bundle.ScreenDensity\x12\x33\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32\x1d.android.bundle.ScreenDensity\"8\n\x11LanguageTargeting\x12\r\n\x05value\x18\x01 \x03(\t\x12\x14\n\x0c\x61lternatives\x18\x02 \x03(\t\"r\n\x13SdkVersionTargeting\x12)\n\x05value\x18\x01 \x03(\x0b\x32\x1a.android.bundle.SdkVersion\x12\x30\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32\x1a.android.bundle.SdkVersion\"\x9c\x01\n!TextureCompressionFormatTargeting\x12\x37\n\x05value\x18\x01 \x03(\x0b\x32(.android.bundle.TextureCompressionFormat\x12>\n\x0c\x61lternatives\x18\x02 \x03(\x0b\x32(.android.bundle.TextureCompressionFormat\">\n\x12SanitizerTargeting\x12(\n\x05value\x18\x01 \x03(\x0b\x32\x19.android.bundle.Sanitizer\"Q\n\x16\x44\x65viceFeatureTargeting\x12\x37\n\x10required_feature\x18\x01 \x01(\x0b\x32\x1d.android.bundle.DeviceFeature\"\x80\x01\n\x13\x44\x65viceTierTargeting\x12*\n\x05value\x18\x03 \x03(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x31\n\x0c\x61lternatives\x18\x04 \x03(\x0b\x32\x1b.google.protobuf.Int32ValueJ\x04\x08\x01\x10\x02J\x04\x08\x02\x10\x03\":\n\x13\x43ountrySetTargeting\x12\r\n\x05value\x18\x01 \x03(\t\x12\x14\n\x0c\x61lternatives\x18\x02 \x03(\t\";\n\x14\x44\x65viceGroupTargeting\x12\r\n\x05value\x18\x01 \x03(\t\x12\x14\n\x0c\x61lternatives\x18\x02 \x03(\t\"+\n\x1a\x44\x65viceGroupModuleTargeting\x12\r\n\x05value\x18\x01 \x03(\t\"3\n\x13SdkRuntimeTargeting\x12\x1c\n\x14requires_sdk_runtime\x18\x01 \x01(\x08\x42\x14\n\x12\x63om.android.bundleb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'targeting_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\022com.android.bundle' + _globals['_APKTARGETING'].fields_by_name['device_tier_targeting']._loaded_options = None + _globals['_APKTARGETING'].fields_by_name['device_tier_targeting']._serialized_options = b'\030\001' + _globals['_APKTARGETING'].fields_by_name['country_set_targeting']._loaded_options = None + _globals['_APKTARGETING'].fields_by_name['country_set_targeting']._serialized_options = b'\030\001' + _globals['_ASSETSDIRECTORYTARGETING'].fields_by_name['device_tier']._loaded_options = None + _globals['_ASSETSDIRECTORYTARGETING'].fields_by_name['device_tier']._serialized_options = b'\030\001' + _globals['_ASSETSDIRECTORYTARGETING'].fields_by_name['country_set']._loaded_options = None + _globals['_ASSETSDIRECTORYTARGETING'].fields_by_name['country_set']._serialized_options = b'\030\001' + _globals['_VARIANTTARGETING']._serialized_start=68 + _globals['_VARIANTTARGETING']._serialized_end=510 + _globals['_APKTARGETING']._serialized_start=513 + _globals['_APKTARGETING']._serialized_end=1231 + _globals['_MODULETARGETING']._serialized_start=1234 + _globals['_MODULETARGETING']._serialized_end=1549 + _globals['_ASSETMODULETARGETING']._serialized_start=1552 + _globals['_ASSETMODULETARGETING']._serialized_end=1724 + _globals['_USERCOUNTRIESTARGETING']._serialized_start=1726 + _globals['_USERCOUNTRIESTARGETING']._serialized_end=1790 + _globals['_SCREENDENSITY']._serialized_start=1793 + _globals['_SCREENDENSITY']._serialized_end=2046 + _globals['_SCREENDENSITY_DENSITYALIAS']._serialized_start=1902 + _globals['_SCREENDENSITY_DENSITYALIAS']._serialized_end=2029 + _globals['_SDKVERSION']._serialized_start=2048 + _globals['_SDKVERSION']._serialized_end=2102 + _globals['_TEXTURECOMPRESSIONFORMAT']._serialized_start=2105 + _globals['_TEXTURECOMPRESSIONFORMAT']._serialized_end=2409 + _globals['_TEXTURECOMPRESSIONFORMAT_TEXTURECOMPRESSIONFORMATALIAS']._serialized_start=2221 + _globals['_TEXTURECOMPRESSIONFORMAT_TEXTURECOMPRESSIONFORMATALIAS']._serialized_end=2409 + _globals['_ABI']._serialized_start=2412 + _globals['_ABI']._serialized_end=2610 + _globals['_ABI_ABIALIAS']._serialized_start=2465 + _globals['_ABI_ABIALIAS']._serialized_end=2610 + _globals['_MULTIABI']._serialized_start=2612 + _globals['_MULTIABI']._serialized_end=2656 + _globals['_SANITIZER']._serialized_start=2658 + _globals['_SANITIZER']._serialized_end=2769 + _globals['_SANITIZER_SANITIZERALIAS']._serialized_start=2728 + _globals['_SANITIZER_SANITIZERALIAS']._serialized_end=2769 + _globals['_DEVICEFEATURE']._serialized_start=2771 + _globals['_DEVICEFEATURE']._serialized_end=2833 + _globals['_ASSETSDIRECTORYTARGETING']._serialized_start=2836 + _globals['_ASSETSDIRECTORYTARGETING']._serialized_end=3235 + _globals['_NATIVEDIRECTORYTARGETING']._serialized_start=3238 + _globals['_NATIVEDIRECTORYTARGETING']._serialized_end=3428 + _globals['_APEXIMAGETARGETING']._serialized_start=3430 + _globals['_APEXIMAGETARGETING']._serialized_end=3504 + _globals['_ABITARGETING']._serialized_start=3506 + _globals['_ABITARGETING']._serialized_end=3599 + _globals['_MULTIABITARGETING']._serialized_start=3601 + _globals['_MULTIABITARGETING']._serialized_end=3709 + _globals['_SCREENDENSITYTARGETING']._serialized_start=3711 + _globals['_SCREENDENSITYTARGETING']._serialized_end=3834 + _globals['_LANGUAGETARGETING']._serialized_start=3836 + _globals['_LANGUAGETARGETING']._serialized_end=3892 + _globals['_SDKVERSIONTARGETING']._serialized_start=3894 + _globals['_SDKVERSIONTARGETING']._serialized_end=4008 + _globals['_TEXTURECOMPRESSIONFORMATTARGETING']._serialized_start=4011 + _globals['_TEXTURECOMPRESSIONFORMATTARGETING']._serialized_end=4167 + _globals['_SANITIZERTARGETING']._serialized_start=4169 + _globals['_SANITIZERTARGETING']._serialized_end=4231 + _globals['_DEVICEFEATURETARGETING']._serialized_start=4233 + _globals['_DEVICEFEATURETARGETING']._serialized_end=4314 + _globals['_DEVICETIERTARGETING']._serialized_start=4317 + _globals['_DEVICETIERTARGETING']._serialized_end=4445 + _globals['_COUNTRYSETTARGETING']._serialized_start=4447 + _globals['_COUNTRYSETTARGETING']._serialized_end=4505 + _globals['_DEVICEGROUPTARGETING']._serialized_start=4507 + _globals['_DEVICEGROUPTARGETING']._serialized_end=4566 + _globals['_DEVICEGROUPMODULETARGETING']._serialized_start=4568 + _globals['_DEVICEGROUPMODULETARGETING']._serialized_end=4611 + _globals['_SDKRUNTIMETARGETING']._serialized_start=4613 + _globals['_SDKRUNTIMETARGETING']._serialized_end=4664 # @@protoc_insertion_point(module_scope) From 42ae00796571ab7cf8b897d25b1ef1ac240ea7a2 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 4 Nov 2025 22:04:18 +0100 Subject: [PATCH 158/291] dist: Target latest Android API level Apps targeting an older SDK can no longer be installed on newer Android devices --- direct/src/dist/commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 50a288b471..6969db6c89 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -300,7 +300,7 @@ class build_apps(setuptools.Command): self.android_version_code = 1 self.android_min_sdk_version = 21 self.android_max_sdk_version = None - self.android_target_sdk_version = 30 + self.android_target_sdk_version = 36 self.android_manifest_file = None self.gui_apps = {} self.console_apps = {} @@ -876,6 +876,7 @@ class build_apps(setuptools.Command): activity.set('android:configChanges', 'layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation') activity.set('android:launchMode', 'singleInstance') activity.set('android:preferMinimalPostProcessing', 'true') + activity.set('android:exported', 'true') act_icon = self.icon_objects.get(appname) if act_icon and act_icon is not app_icon: From a40e8f9fb0f0a254e111c02c01ed278b5609721c Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 4 Nov 2025 22:05:51 +0100 Subject: [PATCH 159/291] android: Fix incorrect usage of ALooper_pollOnce --- panda/src/android/android_main.cxx | 7 +++---- pandatool/src/deploy-stub/android_main.cxx | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/panda/src/android/android_main.cxx b/panda/src/android/android_main.cxx index 55355533c3..177b9ec43d 100644 --- a/panda/src/android/android_main.cxx +++ b/panda/src/android/android_main.cxx @@ -271,10 +271,9 @@ void android_main(struct android_app* app) { // We still need to keep an event loop going until Android gives us leave // to end the process. while (!app->destroyRequested) { - int looper_id; - struct android_poll_source *source; - auto result = ALooper_pollOnce(-1, &looper_id, nullptr, (void **)&source); - if (looper_id == LOOPER_ID_MAIN) { + struct android_poll_source *source = nullptr; + int ident = ALooper_pollOnce(-1, nullptr, nullptr, (void **)&source); + if (ident == LOOPER_ID_MAIN) { int8_t cmd = android_app_read_cmd(app); android_app_pre_exec_cmd(app, cmd); android_app_post_exec_cmd(app, cmd); diff --git a/pandatool/src/deploy-stub/android_main.cxx b/pandatool/src/deploy-stub/android_main.cxx index d1e140c5f3..8134aa893a 100644 --- a/pandatool/src/deploy-stub/android_main.cxx +++ b/pandatool/src/deploy-stub/android_main.cxx @@ -299,10 +299,9 @@ void android_main(struct android_app *app) { // We still need to keep an event loop going until Android gives us leave // to end the process. while (!app->destroyRequested) { - int looper_id; - struct android_poll_source *source; - auto result = ALooper_pollOnce(-1, &looper_id, nullptr, (void **)&source); - if (looper_id == LOOPER_ID_MAIN) { + struct android_poll_source *source = nullptr; + int ident = ALooper_pollOnce(-1, nullptr, nullptr, (void **)&source); + if (ident == LOOPER_ID_MAIN) { int8_t cmd = android_app_read_cmd(app); android_app_pre_exec_cmd(app, cmd); android_app_post_exec_cmd(app, cmd); From cbbd3edc396f06d3bad16f4c7e72577e7476d4d9 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 4 Nov 2025 22:06:17 +0100 Subject: [PATCH 160/291] pandatool: Make WindowsGuid class trivially copyable Squelches compiler warning --- pandatool/src/xfile/windowsGuid.I | 24 ------------------------ pandatool/src/xfile/windowsGuid.h | 12 +++++------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/pandatool/src/xfile/windowsGuid.I b/pandatool/src/xfile/windowsGuid.I index 5d0ce22046..434182d70a 100644 --- a/pandatool/src/xfile/windowsGuid.I +++ b/pandatool/src/xfile/windowsGuid.I @@ -11,14 +11,6 @@ * @date 2004-10-03 */ -/** - * - */ -INLINE WindowsGuid:: -WindowsGuid() { - memset(this, 0, sizeof(WindowsGuid)); -} - /** * */ @@ -42,22 +34,6 @@ WindowsGuid(unsigned long data1, { } -/** - * - */ -INLINE WindowsGuid:: -WindowsGuid(const WindowsGuid ©) { - (*this) = copy; -} - -/** - * - */ -INLINE void WindowsGuid:: -operator = (const WindowsGuid ©) { - memcpy(this, ©, sizeof(WindowsGuid)); -} - /** * */ diff --git a/pandatool/src/xfile/windowsGuid.h b/pandatool/src/xfile/windowsGuid.h index ea79d0be5a..8c0e306993 100644 --- a/pandatool/src/xfile/windowsGuid.h +++ b/pandatool/src/xfile/windowsGuid.h @@ -25,14 +25,12 @@ */ class WindowsGuid { public: - INLINE WindowsGuid(); + constexpr WindowsGuid() = default; INLINE WindowsGuid(unsigned long data1, unsigned short data2, unsigned short data3, unsigned char b1, unsigned char b2, unsigned char b3, unsigned char b4, unsigned char b5, unsigned char b6, unsigned char b7, unsigned char b8); - INLINE WindowsGuid(const WindowsGuid ©); - INLINE void operator = (const WindowsGuid ©); INLINE bool operator == (const WindowsGuid &other) const; INLINE bool operator != (const WindowsGuid &other) const; @@ -45,10 +43,10 @@ public: void output(std::ostream &out) const; private: - unsigned long _data1; - unsigned short _data2; - unsigned short _data3; - unsigned char _b1, _b2, _b3, _b4, _b5, _b6, _b7, _b8; + unsigned long _data1 = 0; + unsigned short _data2 = 0; + unsigned short _data3 = 0; + unsigned char _b1 = 0, _b2 = 0, _b3 = 0, _b4 = 0, _b5 = 0, _b6 = 0, _b7 = 0, _b8 = 0; }; INLINE std::ostream &operator << (std::ostream &out, const WindowsGuid &guid); From 4a7e5288c04fa2ce5ec5a38d7beb9c8b1dc38d80 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 4 Nov 2025 22:07:37 +0100 Subject: [PATCH 161/291] express: Fix compiler warning on Android --- panda/src/express/virtualFileMountAndroidAsset.h | 1 - 1 file changed, 1 deletion(-) diff --git a/panda/src/express/virtualFileMountAndroidAsset.h b/panda/src/express/virtualFileMountAndroidAsset.h index 1caccf98f5..d640e8612e 100644 --- a/panda/src/express/virtualFileMountAndroidAsset.h +++ b/panda/src/express/virtualFileMountAndroidAsset.h @@ -74,7 +74,6 @@ private: private: AAsset *_asset; - off_t _offset; friend class VirtualFileMountAndroidAsset; }; From ec9a7d1e85623a6bb2b42ef1ea105c823a397273 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 4 Nov 2025 23:46:46 +0100 Subject: [PATCH 162/291] dist: Check provided AndroidManifest.xml file for basic sanity --- direct/src/dist/commands.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 6969db6c89..45ceb3c44e 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -627,6 +627,11 @@ class build_apps(setuptools.Command): # Generate an AndroidManifest.xml if none was provided manifest_path = os.path.join(build_dir, 'AndroidManifest.xml') if self.android_manifest_file: + try: + self.check_android_manifest(self.android_manifest_file) + except Exception as e: + self.announce(f"Failed to use provided manifest file from {self.android_manifest_file}", distutils.log.FATAL) + raise self.copy(self.android_manifest_file, manifest_path) else: self.generate_android_manifest(manifest_path) @@ -895,6 +900,38 @@ class build_apps(setuptools.Command): with open(path, 'wb') as fh: tree.write(fh, encoding='utf-8', xml_declaration=True) + def check_android_manifest(self, path): + """ Checks that the user-provided manifest file seems OK. """ + + # This function doesn't aim to check everything as it's the user's + # responsibility, just a basic sanity check, but if we change anything + # in our own generation logic then it would be good to check those + # things here and warn if anything needs to be updated. + + import xml.etree.ElementTree as ET + + android = '{http://schemas.android.com/apk/res/android}' + + tree = ET.parse(path) + root = tree.getroot() + if root.tag != 'manifest': + raise RuntimeError(f"Expected in {path}") + + if root.attrib['package'] != self.application_id: + raise RuntimeError(f" package attribute does not match given application_id {self.application_id}") + + apps = root.findall('application') + if len(apps) != 1: + raise RuntimeError(" must contain exactly one ") + + application = apps[0] + for activity in application.iter('activity'): + if f'{android}name' not in activity.attrib: + raise RuntimeError(" element must have android:name attribute") + + if self.android_target_sdk_version >= 31 and f'{android}exported' not in activity.attrib: + raise RuntimeError(" element must have android:exported attribute when targeting Android API 31+") + def build_binaries(self, platform, binary_dir, data_dir=None): """ Builds the binary data for the given platform. """ From 1ea0ea6d05d8d265e45901128d959ec83e22789f Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 4 Nov 2025 23:48:46 +0100 Subject: [PATCH 163/291] dist: Fix for gui_apps with spaces in name on Android --- direct/src/dist/commands.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 45ceb3c44e..2ba9524c8b 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -611,7 +611,8 @@ class build_apps(setuptools.Command): # Conventional name for icon on Android. basename = 'ic_launcher.png' else: - basename = f'ic_{appname}.png' + appname_sane = appname.replace(' ', '_') + basename = f'ic_{appname_sane}.png' res_dir = os.path.join(build_dir, 'res') icon.writeSize(48, os.path.join(res_dir, 'mipmap-mdpi-v4', basename)) @@ -873,6 +874,8 @@ class build_apps(setuptools.Command): application.set('android:icon', '@mipmap/ic_launcher') for appname in self.gui_apps: + appname_sane = appname.replace(' ', '_') + activity = ET.SubElement(application, 'activity') activity.set('android:name', 'org.panda3d.android.PythonActivity') activity.set('android:label', appname) @@ -885,11 +888,11 @@ class build_apps(setuptools.Command): act_icon = self.icon_objects.get(appname) if act_icon and act_icon is not app_icon: - activity.set('android:icon', '@mipmap/ic_' + appname) + activity.set('android:icon', '@mipmap/ic_' + appname_sane) meta_data = ET.SubElement(activity, 'meta-data') meta_data.set('android:name', 'android.app.lib_name') - meta_data.set('android:value', appname) + meta_data.set('android:value', appname_sane) intent_filter = ET.SubElement(activity, 'intent-filter') ET.SubElement(intent_filter, 'action').set('android:name', 'android.intent.action.MAIN') @@ -1134,7 +1137,8 @@ class build_apps(setuptools.Command): elif platform.startswith('android'): if not use_console: stub_name = 'libdeploy-stubw.so' - target_name = 'lib' + target_name + '.so' + appname_sane = appname.replace(' ', '_') + target_name = 'lib' + appname_sane + '.so' if platform.startswith('win'): stub_name += '.exe' From 418d041252ab55850258012399bc63938552f65b Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 4 Nov 2025 23:49:44 +0100 Subject: [PATCH 164/291] pipeline: Fixes for fragile TLS implementation on Android --- panda/src/pipeline/threadPosixImpl.I | 6 +++++ panda/src/pipeline/threadPosixImpl.cxx | 26 +++++++++++++++++++++- panda/src/pipeline/threadPosixImpl.h | 10 ++++++++- pandatool/src/deploy-stub/android_main.cxx | 3 ++- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/panda/src/pipeline/threadPosixImpl.I b/panda/src/pipeline/threadPosixImpl.I index 326868ab61..cde10d4175 100644 --- a/panda/src/pipeline/threadPosixImpl.I +++ b/panda/src/pipeline/threadPosixImpl.I @@ -46,8 +46,14 @@ prepare_for_exit() { INLINE Thread *ThreadPosixImpl:: get_current_thread() { TAU_PROFILE("Thread *ThreadPosixImpl::get_current_thread()", " ", TAU_USER); +#ifdef ANDROID + // Android doesn't correctly share TLS variables across multiple DSOs, so + // we can't inline this. + return do_get_current_thread(); +#else Thread *thread = _current_thread; return (thread != nullptr) ? thread : init_current_thread(); +#endif } /** diff --git a/panda/src/pipeline/threadPosixImpl.cxx b/panda/src/pipeline/threadPosixImpl.cxx index 9266ded933..9a7598f3df 100644 --- a/panda/src/pipeline/threadPosixImpl.cxx +++ b/panda/src/pipeline/threadPosixImpl.cxx @@ -33,7 +33,12 @@ static JavaVM *java_vm = nullptr; #endif +// See comment in header file. +#ifdef ANDROID +static __thread Thread *_current_thread = nullptr; +#else __thread Thread *ThreadPosixImpl::_current_thread = nullptr; +#endif static patomic_flag _main_thread_known = ATOMIC_FLAG_INIT; /** @@ -53,6 +58,10 @@ ThreadPosixImpl:: _detached = true; } + if (_current_thread == _parent_obj) { + _current_thread = nullptr; + } + _mutex.unlock(); } @@ -332,7 +341,9 @@ root_func(void *data) { #ifdef ANDROID // We cannot let the thread end without detaching it. if (self->_jni_env != nullptr) { - java_vm->DetachCurrentThread(); + if (java_vm != nullptr) { + java_vm->DetachCurrentThread(); + } self->_jni_env = nullptr; } #endif @@ -341,6 +352,8 @@ root_func(void *data) { // might delete the parent object, and in turn, delete the ThreadPosixImpl // object. unref_delete(self->_parent_obj); + + _current_thread = nullptr; } return nullptr; @@ -360,6 +373,17 @@ init_current_thread() { return thread; } +#ifdef ANDROID +/** + * + */ +Thread *ThreadPosixImpl:: +do_get_current_thread() { + Thread *thread = _current_thread; + return (thread != nullptr) ? thread : init_current_thread(); +} +#endif // ANDROID + #ifdef ANDROID /** * Called by Java when loading this library from the Java virtual machine. diff --git a/panda/src/pipeline/threadPosixImpl.h b/panda/src/pipeline/threadPosixImpl.h index 34422506d7..bb26f615e1 100644 --- a/panda/src/pipeline/threadPosixImpl.h +++ b/panda/src/pipeline/threadPosixImpl.h @@ -48,7 +48,7 @@ public: INLINE static void prepare_for_exit(); - INLINE static Thread *get_current_thread(); + static Thread *get_current_thread(); static Thread *bind_thread(Thread *thread); INLINE static bool is_threading_supported(); INLINE static bool is_true_threads(); @@ -69,6 +69,10 @@ private: static void *root_func(void *data); static Thread *init_current_thread(); +#ifdef ANDROID + static Thread *do_get_current_thread(); +#endif + // There appears to be a name collision with the word "Status". enum PStatus { S_new, @@ -88,7 +92,11 @@ private: JNIEnv *_jni_env; #endif + // Android doesn't do TLS correctly across .so boundaries, so we hide the + // current thread in the .cxx file. +#ifndef ANDROID static __thread Thread *_current_thread; +#endif }; #include "threadPosixImpl.I" diff --git a/pandatool/src/deploy-stub/android_main.cxx b/pandatool/src/deploy-stub/android_main.cxx index 8134aa893a..4e1bb7c260 100644 --- a/pandatool/src/deploy-stub/android_main.cxx +++ b/pandatool/src/deploy-stub/android_main.cxx @@ -118,7 +118,8 @@ void android_main(struct android_app *app) { PT(Thread) current_thread = Thread::bind_thread(thread_name, "android_app"); android_cat.info() - << "New native activity started on " << *current_thread << "\n"; + << "New native activity started on " << *current_thread + << " (" << current_thread << ")\n"; // Fetch the data directory. jmethodID get_appinfo = env->GetMethodID(activity_class, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;"); From 5c70817708ea4923616411a89ff2ad4747c93c77 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 5 Nov 2025 12:46:50 +0100 Subject: [PATCH 165/291] workflow: Update to macOS 15 --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ee75a67f9..0844608a36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: double: NO #- profile: macos-coverage-unity-xcode - # os: macOS-13 + # os: macOS-15 # config: Coverage # unity: YES # generator: Xcode @@ -53,7 +53,7 @@ jobs: # double: NO - profile: macos-nometa-standard-makefile - os: macOS-13 + os: macOS-15 config: Standard unity: NO generator: Unix Makefiles @@ -372,7 +372,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: - os: [ubuntu-22.04, windows-2022, macOS-13] + os: [ubuntu-22.04, windows-2022, macOS-15] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 From db27278f2254351968dcfeb38cd6706491b27477 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 5 Nov 2025 12:47:04 +0100 Subject: [PATCH 166/291] distributed: Fix for importing CR without OpenSSL compiled-in --- direct/src/distributed/ConnectionRepository.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/direct/src/distributed/ConnectionRepository.py b/direct/src/distributed/ConnectionRepository.py index a29c61cafa..f3ddc96f4f 100644 --- a/direct/src/distributed/ConnectionRepository.py +++ b/direct/src/distributed/ConnectionRepository.py @@ -1,4 +1,4 @@ -from panda3d.core import DocumentSpec, Filename, HTTPClient, VirtualFileSystem, getModelPath +from panda3d.core import DocumentSpec, Filename, VirtualFileSystem, getModelPath from panda3d.direct import CConnectionRepository, DCPacker from direct.task import Task from direct.task.TaskManagerGlobal import taskMgr @@ -590,6 +590,7 @@ class ConnectionRepository( if self.http is None: try: + from panda3d.core import HTTPClient self.http = HTTPClient() except Exception: pass From e524dc9766e532573712a8d7cceff5eaa090b756 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 5 Nov 2025 12:47:40 +0100 Subject: [PATCH 167/291] dtoolbase: Apply pvector libc++ workaround across the board The bug with empty vector_uchar causing a crash seems to occur on Android as well --- dtool/src/dtoolbase/pvector.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dtool/src/dtoolbase/pvector.h b/dtool/src/dtoolbase/pvector.h index 7561047add..12e7c108ef 100644 --- a/dtool/src/dtoolbase/pvector.h +++ b/dtool/src/dtoolbase/pvector.h @@ -51,13 +51,9 @@ public: pvector(pvector &&from) noexcept : base_class(std::move(from)) {}; explicit pvector(size_type n, TypeHandle type_handle = pvector_type_handle) : base_class(n, Type(), allocator(type_handle)) { } explicit pvector(size_type n, const Type &value, TypeHandle type_handle = pvector_type_handle) : base_class(n, value, allocator(type_handle)) { } -#ifdef __APPLE__ pvector(const Type *begin, const Type *end, TypeHandle type_handle = pvector_type_handle) : base_class(allocator(type_handle)) { this->insert(this->end(), begin, end); } -#else - pvector(const Type *begin, const Type *end, TypeHandle type_handle = pvector_type_handle) : base_class(begin, end, allocator(type_handle)) { } -#endif pvector(std::initializer_list init, TypeHandle type_handle = pvector_type_handle) : base_class(std::move(init), allocator(type_handle)) { } pvector &operator =(const pvector ©) { From e9fddf68399e8e073fd6baec1d186e0b87aff4ec Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 5 Nov 2025 12:48:29 +0100 Subject: [PATCH 168/291] makepanda: Use correct .so suffix on Android --- makepanda/makepandacore.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index d3ec9e4078..9879469b25 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -3426,6 +3426,10 @@ def GetExtensionSuffix(): abi = GetPythonABI() arch = GetTargetArch() return '.{0}-{1}-emscripten.so'.format(abi, arch) + elif target == 'android': + abi = GetPythonABI() + triple = ANDROID_TRIPLE.rstrip('0123456789') + return '.{0}-{1}.so'.format(abi, triple) elif CrossCompiling(): return '.{0}.so'.format(GetPythonABI()) else: From 90ca53ac2d3111e893e7eaf6974ce6117c97f69f Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 5 Nov 2025 12:50:30 +0100 Subject: [PATCH 169/291] android: Fixes to allow Panda to run from the adb shell --- dtool/src/prc/configPageManager.cxx | 6 +-- dtool/src/prc/notify.cxx | 49 ++++++++++++------- dtool/src/prc/pnotify.h | 2 +- .../androiddisplay/androidGraphicsPipe.cxx | 5 ++ panda/src/pipeline/threadPosixImpl.cxx | 21 ++++---- panda/src/pipeline/threadPosixImpl.h | 2 +- 6 files changed, 53 insertions(+), 32 deletions(-) diff --git a/dtool/src/prc/configPageManager.cxx b/dtool/src/prc/configPageManager.cxx index 7821617bfd..e3dbcfa69b 100644 --- a/dtool/src/prc/configPageManager.cxx +++ b/dtool/src/prc/configPageManager.cxx @@ -97,7 +97,6 @@ reload_implicit_pages() { } _implicit_pages.clear(); -#ifndef ANDROID // If we are running inside a deployed application, see if it exposes // information about how the PRC data should be initialized. struct BlobInfo { @@ -129,11 +128,13 @@ reload_implicit_pages() { // const BlobInfo *blobinfo = (const BlobInfo *)dlsym(RTLD_SELF, "blobinfo"); #elif defined(__EMSCRIPTEN__) const BlobInfo *blobinfo = nullptr; +#elif defined(ANDROID) + const BlobInfo *blobinfo = nullptr; #else const BlobInfo *blobinfo = (const BlobInfo *)dlsym(dlopen(nullptr, RTLD_NOW), "blobinfo"); #endif if (blobinfo == nullptr) { -#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) +#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && !defined(ANDROID) // Clear the error flag. dlerror(); #endif @@ -482,7 +483,6 @@ reload_implicit_pages() { } } } -#endif // ANDROID if (!_loaded_implicit) { config_initialized(); diff --git a/dtool/src/prc/notify.cxx b/dtool/src/prc/notify.cxx index 5c4010264b..e3615b583a 100644 --- a/dtool/src/prc/notify.cxx +++ b/dtool/src/prc/notify.cxx @@ -27,6 +27,7 @@ #endif #ifdef ANDROID +#include #include #include "androidLogStream.h" #endif @@ -635,22 +636,7 @@ config_initialized() { // notify-output even after the initial import of Panda3D modules. However, // it cannot be changed after the first time it is set. -#if defined(ANDROID) - // Android redirects stdio and stderr to /dev/null, - // but does provide its own logging system. We use a special - // type of stream that redirects it to Android's log system. - - Notify *ptr = Notify::ptr(); - - for (int severity = 0; severity <= NS_fatal; ++severity) { - int priority = ANDROID_LOG_UNKNOWN; - if (severity != NS_unspecified) { - priority = severity + 1; - } - ptr->_log_streams[severity] = new AndroidLogStream(priority); - } - -#elif defined(__EMSCRIPTEN__) +#if defined(__EMSCRIPTEN__) // We have no writable filesystem in JavaScript. Instead, we set up a // special stream that logs straight into the Javascript console. @@ -715,11 +701,38 @@ config_initialized() { } #endif // BUILD_IPHONE } + #ifdef ANDROID + for (int severity = 0; severity <= NS_fatal; ++severity) { + ptr->_log_streams[severity] = ptr->_ostream_ptr; + } + } else { - // By default, we always redirect the notify stream to the Android log. + // By default, we always redirect the notify stream to the Android log, + // except if we are running from the adb shell. We decide this based + // on whether stderr is redirected to /dev/null. Notify *ptr = Notify::ptr(); - ptr->set_ostream_ptr(new AndroidLogStream(ANDROID_LOG_INFO), true); + struct stat a, b; + if (fstat(STDERR_FILENO, &a) == 0 && stat("/dev/null", &b) == 0 && + a.st_dev == b.st_dev && a.st_ino == b.st_ino) { + // Android redirects stdio and stderr to /dev/null, + // but does provide its own logging system. We use a special + // type of stream that redirects it to Android's log system. + for (int severity = 0; severity <= NS_fatal; ++severity) { + int priority = ANDROID_LOG_UNKNOWN; + if (severity != NS_unspecified) { + priority = severity + 1; + } + ptr->_log_streams[severity] = new AndroidLogStream(priority); + } + ptr->set_ostream_ptr(new AndroidLogStream(ANDROID_LOG_INFO), true); + } else { + // Running from the terminal, set all the log streams to point to the + // same output. + for (int severity = 0; severity <= NS_fatal; ++severity) { + ptr->_log_streams[severity] = &cerr; + } + } #endif } } diff --git a/dtool/src/prc/pnotify.h b/dtool/src/prc/pnotify.h index 76ab5be7fb..2c949f1e1a 100644 --- a/dtool/src/prc/pnotify.h +++ b/dtool/src/prc/pnotify.h @@ -102,7 +102,7 @@ private: Categories _categories; #if defined(ANDROID) - AndroidLogStream *_log_streams[NS_fatal + 1]; + std::ostream *_log_streams[NS_fatal + 1]; #elif defined(__EMSCRIPTEN__) EmscriptenLogStream *_log_streams[NS_fatal + 1]; #endif diff --git a/panda/src/androiddisplay/androidGraphicsPipe.cxx b/panda/src/androiddisplay/androidGraphicsPipe.cxx index 53f0f72310..e78c53878f 100644 --- a/panda/src/androiddisplay/androidGraphicsPipe.cxx +++ b/panda/src/androiddisplay/androidGraphicsPipe.cxx @@ -19,6 +19,8 @@ #include "config_androiddisplay.h" #include "frameBufferProperties.h" +extern IMPORT_CLASS struct android_app *panda_android_app; + TypeHandle AndroidGraphicsPipe::_type_handle; /** @@ -121,6 +123,9 @@ make_output(const std::string &name, // First thing to try: an eglGraphicsWindow if (retry == 0) { + if (panda_android_app == nullptr) { + return nullptr; + } if (((flags&BF_require_parasite)!=0)|| ((flags&BF_refuse_window)!=0)|| ((flags&BF_resizeable)!=0)|| diff --git a/panda/src/pipeline/threadPosixImpl.cxx b/panda/src/pipeline/threadPosixImpl.cxx index 9a7598f3df..debea8f096 100644 --- a/panda/src/pipeline/threadPosixImpl.cxx +++ b/panda/src/pipeline/threadPosixImpl.cxx @@ -223,12 +223,15 @@ bind_thread(Thread *thread) { #ifdef ANDROID /** - * Attaches the thread to the Java virtual machine. If this returns true, a - * JNIEnv pointer can be acquired using get_jni_env(). + * Attaches the thread to the Java virtual machine. On success, returns a + * JNIEnv pointer; returns nullptr otherwise, in which case the application + * might not be running inside a Java VM. */ -bool ThreadPosixImpl:: +JNIEnv *ThreadPosixImpl:: attach_java_vm() { - assert(java_vm != nullptr); + if (java_vm == nullptr) { + return nullptr; + } JNIEnv *env; std::string thread_name = _parent_obj->get_name(); @@ -241,10 +244,10 @@ attach_java_vm() { << "Failed to attach Java VM to thread " << _parent_obj->get_name() << "!\n"; _jni_env = nullptr; - return false; + return nullptr; } _jni_env = env; - return true; + return env; } /** @@ -317,7 +320,7 @@ root_func(void *data) { #ifdef ANDROID // Attach the Java VM to allow calling Java functions in this thread. - self->attach_java_vm(); + JNIEnv *jni_env = self->attach_java_vm(); #endif self->_parent_obj->thread_main(); @@ -340,11 +343,11 @@ root_func(void *data) { #ifdef ANDROID // We cannot let the thread end without detaching it. - if (self->_jni_env != nullptr) { + if (jni_env != nullptr) { if (java_vm != nullptr) { java_vm->DetachCurrentThread(); } - self->_jni_env = nullptr; + jni_env = nullptr; } #endif diff --git a/panda/src/pipeline/threadPosixImpl.h b/panda/src/pipeline/threadPosixImpl.h index bb26f615e1..7859c2897d 100644 --- a/panda/src/pipeline/threadPosixImpl.h +++ b/panda/src/pipeline/threadPosixImpl.h @@ -59,7 +59,7 @@ public: #ifdef ANDROID INLINE JNIEnv *get_jni_env() const; - bool attach_java_vm(); + JNIEnv *attach_java_vm(); static void bind_java_thread(); #endif From 1047728429aa9271433eff2ad2e7d65717d37654 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 5 Nov 2025 12:52:47 +0100 Subject: [PATCH 170/291] tests: Support running test suite from adb shell --- makepanda/makepanda.py | 21 +++++++++++---------- tests/main.c | 5 +++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 5441a5215c..e0bb5d5061 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -5973,7 +5973,7 @@ if PkgSkip("PYTHON") == 0: # # Build the test runner for static builds # -if GetLinkAllStatic(): +if GetLinkAllStatic() or GetTarget() == 'android': if GetTarget() == 'emscripten': LinkFlag('RUN_TESTS_FLAGS', '-s NODERAWFS') LinkFlag('RUN_TESTS_FLAGS', '-s ASSERTIONS=2') @@ -5994,15 +5994,16 @@ if GetLinkAllStatic(): OPTS=['DIR:tests', 'PYTHON', 'RUN_TESTS_FLAGS', 'SUBSYSTEM:CONSOLE'] PyTargetAdd('run_tests-main.obj', opts=OPTS, input='main.c') PyTargetAdd('run_tests.exe', input='run_tests-main.obj') - PyTargetAdd('run_tests.exe', input='core.pyd') - if not PkgSkip('DIRECT'): - PyTargetAdd('run_tests.exe', input='direct.pyd') - if not PkgSkip('PANDAPHYSICS'): - PyTargetAdd('run_tests.exe', input='physics.pyd') - if not PkgSkip('EGG'): - PyTargetAdd('run_tests.exe', input='egg.pyd') - if not PkgSkip('BULLET'): - PyTargetAdd('run_tests.exe', input='bullet.pyd') + if GetLinkAllStatic(): + PyTargetAdd('run_tests.exe', input='core.pyd') + if not PkgSkip('DIRECT'): + PyTargetAdd('run_tests.exe', input='direct.pyd') + if not PkgSkip('PANDAPHYSICS'): + PyTargetAdd('run_tests.exe', input='physics.pyd') + if not PkgSkip('EGG'): + PyTargetAdd('run_tests.exe', input='egg.pyd') + if not PkgSkip('BULLET'): + PyTargetAdd('run_tests.exe', input='bullet.pyd') PyTargetAdd('run_tests.exe', input=COMMON_PANDA_LIBS) PyTargetAdd('run_tests.exe', opts=['PYTHON', 'BULLET', 'RUN_TESTS_FLAGS']) diff --git a/tests/main.c b/tests/main.c index 8fce2d6f92..fe49ed14fc 100644 --- a/tests/main.c +++ b/tests/main.c @@ -111,6 +111,11 @@ int main(int argc, char **argv) { PyRun_SimpleString("import sys; sys.argv.insert(1, '--capture=sys')"); #endif +#ifdef ANDROID + // No caching on Android + PyRun_SimpleString("import sys; sys.argv.insert(1, '-o cache_dir=/dev/null')"); +#endif + return Py_RunMain(); exception: From bba39286c759ca3d534fa4f46f586504006c9a8f Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 5 Nov 2025 13:22:08 +0100 Subject: [PATCH 171/291] workflow: Set XCode version to 16 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0844608a36..3bb5764be4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,7 +110,7 @@ jobs: - name: Set up XCode (macOS) if: runner.os == 'macOS' - run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer - name: Install dependencies (Ubuntu) if: startsWith(matrix.os, 'ubuntu') @@ -400,7 +400,7 @@ jobs: - name: Set up XCode (macOS) if: runner.os == 'macOS' - run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer - name: Set up Python 3.13 uses: actions/setup-python@v6 From d1aeef5c25b8af15cbeed0d8f03d5a8c7807492d Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 7 Nov 2025 09:30:39 +0100 Subject: [PATCH 172/291] makepanda: Remove -m32 and -m64 flags for Android --- makepanda/makepanda.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index e0bb5d5061..b9f68d9c1a 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1382,10 +1382,10 @@ def CompileCxx(obj,src,opts): elif arch == 'mips64': cmd += ' -fintegrated-as' elif arch == 'x86': - cmd += ' -march=i686 -mssse3 -mfpmath=sse -m32' + cmd += ' -march=i686 -mssse3 -mfpmath=sse' cmd += ' -mstackrealign' elif arch == 'x86_64': - cmd += ' -march=x86-64 -msse4.2 -mpopcnt -m64' + cmd += ' -march=x86-64 -msse4.2 -mpopcnt' cmd += " -Wa,--noexecstack" From 27d905feff948f3913b8758ece654cc338e81f6b Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 7 Nov 2025 10:36:00 +0100 Subject: [PATCH 173/291] workflow: Add Android build to CI matrix --- .github/workflows/ci.yml | 153 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bb5764be4..066cb3c4a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -552,3 +552,156 @@ jobs: shell: bash run: | PYTHONHOME=~/python/usr/local PYTHONPATH=built node built/bin/run_tests.js tests + + android: + if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" + + strategy: + fail-fast: false + + matrix: + include: + - android-abi: arm64-v8a + android-arch: arm64 + android-triple: aarch64-linux-android + ndk-version: 29.0.14206865 + runner: ubuntu-latest + + - android-abi: armeabi-v7a + android-arch: arm + android-triple: arm-linux-androideabi + ndk-version: 29.0.14206865 + runner: ubuntu-latest + + - android-abi: x86_64 + android-arch: x86_64 + android-triple: x86_64-linux-android + ndk-version: 29.0.14206865 + runner: ubuntu-latest + + - android-abi: x86 + android-arch: x86 + android-triple: i686-linux-android + ndk-version: 29.0.14206865 + runner: ubuntu-latest + + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v5 + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + with: + packages: 'tools platform-tools platforms;android-21 ndk;${{ matrix.ndk-version }}' + + - name: Set up Android NDK + run: | + echo "ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/${{ matrix.ndk-version }}" >> $GITHUB_ENV + + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: Restore Python build cache + id: cache-android-python-restore + uses: actions/cache/restore@v4 + with: + path: ~/python-prefix + key: cache-android-python-3.13.9-${{ matrix.android-abi }}-ndk${{ matrix.ndk-version }} + + - name: Build Python 3.13 + if: steps.cache-android-python-restore.outputs.cache-hit != 'true' + run: | + wget https://www.python.org/ftp/python/3.13.9/Python-3.13.9.tar.xz + tar -xJf Python-3.13.9.tar.xz + sed -i.bak "s/aarch64-linux-android/${{ matrix.android-triple }}/" Python-3.13.9/Android/android.py + sed -i.bak "s/ndk_version=[0-9.]\{1,\}/ndk_version=${{ matrix.ndk-version }}/" Python-3.13.9/Android/android-env.sh + (cd Python-3.13.9/Android && python3 android.py build ${{ matrix.android-triple }}) + cp -R Python-3.13.9/cross-build/${{ matrix.android-triple }}/prefix ~/python-prefix + rm -rf Python-3.13.9 + + - name: Save Python build cache + id: cache-android-python-save + if: steps.cache-android-python-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ~/python-prefix + key: ${{ steps.cache-android-python-restore.outputs.cache-primary-key }} + + - name: Restore thirdparty build cache + id: cache-android-thirdparty-restore + uses: actions/cache/restore@v4 + with: + path: ./thirdparty + key: cache-android-thirdparty-afc7ad98aee90da0f2f6bad826a41f9b71e0cdd0-${{ matrix.android-abi }}-ndk${{ matrix.ndk-version }} + + - name: Install yasm + if: steps.cache-android-thirdparty-restore.outputs.cache-hit != 'true' + run: ${{runner.os == 'macOS' && 'brew' || 'sudo apt-get'}} install yasm + + - name: Build thirdparty packages + if: steps.cache-android-thirdparty-restore.outputs.cache-hit != 'true' + run: | + git clone --revision=afc7ad98aee90da0f2f6bad826a41f9b71e0cdd0 --depth=1 https://github.com/rdb/panda3d-thirdparty.git thirdparty + cd thirdparty + cmake -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_ROOT/build/cmake/android.toolchain.cmake \ + -DCMAKE_ANDROID_ARCH_ABI=${{ matrix.android-abi }} \ + -DCMAKE_ANDROID_ARCH=${{ matrix.android-arch }} \ + -DCMAKE_SYSTEM_VERSION=21 \ + -DANDROID_ABI=${{ matrix.android-abi }} \ + -DANDROID_PLATFORM=android-21 \ + -DBUILD_FCOLLADA=OFF \ + -DBUILD_OPENSSL=OFF \ + -DBUILD_VRPN=OFF \ + -DBUILD_ARTOOLKIT=OFF + cmake --build build --config Release -j4 + rm -rf build + + - name: Save thirdparty build cache + id: cache-android-thirdparty-save + if: steps.cache-android-thirdparty-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ./thirdparty + key: ${{ steps.cache-android-thirdparty-restore.outputs.cache-primary-key }} + + - name: Build for Android + shell: bash + run: | + python makepanda/makepanda.py --git-commit=${{github.sha}} --target android-21 --python-incdir=~/python-prefix/include --python-libdir=~/python-prefix/lib --arch ${{ matrix.android-arch }} --everything --no-openssl --no-tinydisplay --no-pandatool --verbose --threads=4 + + - name: Install test dependencies + shell: bash + run: | + python -m pip install -t ~/python-prefix/lib/python3.13/site-packages -r requirements-test.txt + + - name: Enable KVM group perms + if: startsWith(matrix.runner, 'ubuntu') + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Setup Android Emulator and Run Tests + if: startsWith(matrix.android-abi, 'x86') + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 27 + arch: ${{ matrix.android-abi }} + target: default + profile: pixel_6 + script: | + adb shell "rm -rf /data/local/tmp/panda3d-tests" + adb push built/bin/run_tests /data/local/tmp/panda3d-tests/run_tests + adb shell "mkdir /data/local/tmp/panda3d-tests/lib" + adb push built/lib/*.so /data/local/tmp/panda3d-tests/lib + adb push ~/python-prefix/lib/*.so /data/local/tmp/panda3d-tests/lib + adb push ~/python-prefix/lib/python3.13 /data/local/tmp/panda3d-tests/lib/python3.13 + adb push built/panda3d /data/local/tmp/panda3d-tests/lib/python3.13/site-packages/ + adb push built/direct /data/local/tmp/panda3d-tests/lib/python3.13/site-packages/ + adb push built/models /data/local/tmp/panda3d-tests/models + adb push built/etc /data/local/tmp/panda3d-tests/etc + adb push tests /data/local/tmp/panda3d-tests/tests + adb shell "cd /data/local/tmp/panda3d-tests && LD_LIBRARY_PATH=/data/local/tmp/panda3d-tests/lib PYTHONPATH=/data/local/tmp/panda3d-tests/lib/python3.13/site-packages ./run_tests -v tests" From 32bdca90150bd0e223098f59a2a86b36b7f6575f Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 8 Nov 2025 23:55:52 +0100 Subject: [PATCH 174/291] android: Work on API level 36 support (though target 35 for now) --- .github/workflows/ci.yml | 4 ++-- direct/src/dist/FreezeTool.py | 3 +++ direct/src/dist/_android.py | 2 ++ direct/src/dist/commands.py | 2 +- direct/src/dist/installers.py | 2 +- dtool/src/dtoolbase/memoryHook.cxx | 3 +++ makepanda/makepanda.py | 5 +++++ 7 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 066cb3c4a4..6e56441a51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -633,7 +633,7 @@ jobs: uses: actions/cache/restore@v4 with: path: ./thirdparty - key: cache-android-thirdparty-afc7ad98aee90da0f2f6bad826a41f9b71e0cdd0-${{ matrix.android-abi }}-ndk${{ matrix.ndk-version }} + key: cache-android-thirdparty-935c80380ca171e08587c570e9dad678d29db3c8-${{ matrix.android-abi }}-ndk${{ matrix.ndk-version }} - name: Install yasm if: steps.cache-android-thirdparty-restore.outputs.cache-hit != 'true' @@ -642,7 +642,7 @@ jobs: - name: Build thirdparty packages if: steps.cache-android-thirdparty-restore.outputs.cache-hit != 'true' run: | - git clone --revision=afc7ad98aee90da0f2f6bad826a41f9b71e0cdd0 --depth=1 https://github.com/rdb/panda3d-thirdparty.git thirdparty + git clone --revision=935c80380ca171e08587c570e9dad678d29db3c8 --depth=1 https://github.com/rdb/panda3d-thirdparty.git thirdparty cd thirdparty cmake -B build \ -DCMAKE_BUILD_TYPE=Release \ diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index 37b697f878..a56df6f951 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -1970,6 +1970,9 @@ class Freezer: elif self.platform.endswith('_aarch64') or self.platform.endswith('_arm64'): # Most arm64 operating systems are configured with 16 KiB pages. blob_align = 16384 + elif self.platform.replace('-', '_') == 'android_x86_64': + # Android nowadays requires 16 KiB pages on 64-bit Intel as well. + blob_align = 16384 else: # Align to page size, so that it can be mmapped. blob_align = 4096 diff --git a/direct/src/dist/_android.py b/direct/src/dist/_android.py index bebb7bf9df..98a88f1fcd 100644 --- a/direct/src/dist/_android.py +++ b/direct/src/dist/_android.py @@ -174,6 +174,7 @@ ANDROID_ATTRIBUTES = { 'debuggable': bool_resource(0x0101000f), 'documentLaunchMode': enum_resource(0x1010445, "none", "intoExisting", "always", "never"), 'enabled': bool_resource(0x101000e), + 'enableOnBackInvokedCallback': bool_resource(0x0101066c), 'excludeFromRecents': bool_resource(0x1010017), 'exported': bool_resource(0x1010010), 'extractNativeLibs': bool_resource(0x10104ea), @@ -195,6 +196,7 @@ ANDROID_ATTRIBUTES = { 'multiprocess': bool_resource(0x1010013), 'name': str_resource(0x1010003), 'noHistory': bool_resource(0x101022d), + 'pageSizeCompat': bool_resource(0x010106ab), 'pathPattern': str_resource(0x101002c), 'preferMinimalPostProcessing': bool_resource(0x101060c), 'required': bool_resource(0x101028e), diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 2ba9524c8b..51a7face50 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -300,7 +300,7 @@ class build_apps(setuptools.Command): self.android_version_code = 1 self.android_min_sdk_version = 21 self.android_max_sdk_version = None - self.android_target_sdk_version = 36 + self.android_target_sdk_version = 35 self.android_manifest_file = None self.gui_apps = {} self.console_apps = {} diff --git a/direct/src/dist/installers.py b/direct/src/dist/installers.py index f09cef7841..1e1ee5d8ea 100644 --- a/direct/src/dist/installers.py +++ b/direct/src/dist/installers.py @@ -251,7 +251,7 @@ def create_aab(command, basename, build_dir): entry.entry_id.id = entry_id entry.name = res_name - for density, tag in (160, 'mdpi'), (240, 'hdpi'), (320, 'xhdpi'), (480, 'xxhdpi'), (640, 'xxxhdpi'): + for density, tag in (120, 'ldpi'), (160, 'mdpi'), (240, 'hdpi'), (320, 'xhdpi'), (480, 'xxhdpi'), (640, 'xxxhdpi'): path = f'res/mipmap-{tag}-v4/{res_name}.png' if (build_dir_fn / path).exists(): bundle.add_subfile('base/' + path, build_dir_fn / path, 0) diff --git a/dtool/src/dtoolbase/memoryHook.cxx b/dtool/src/dtoolbase/memoryHook.cxx index 37b36d8633..b2c51c06a0 100644 --- a/dtool/src/dtoolbase/memoryHook.cxx +++ b/dtool/src/dtoolbase/memoryHook.cxx @@ -618,6 +618,9 @@ determine_page_size() const { _page_size = (size_t)sysinfo.dwPageSize; +#elif defined(ANDROID) + _page_size = getpagesize(); + #else // Posix case. _page_size = sysconf(_SC_PAGESIZE); diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index b9f68d9c1a..9c1a14f081 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1906,6 +1906,11 @@ def CompileLink(dll, obj, opts): cmd += " -march=armv7-a -Wl,--fix-cortex-a8" elif arch == 'mips': cmd += ' -mips32' + + if arch.endswith('64'): + # See https://developer.android.com/guide/practices/page-sizes + cmd += ' -Wl,-z,max-page-size=16384' + cmd += ' -lc -lm' elif GetTarget() == 'emscripten': From 8a23014cc6d1eeba3116046239f25ea42096c21b Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 9 Nov 2025 12:07:26 +0100 Subject: [PATCH 175/291] android: Set Activity title to window title --- panda/src/android/config_android.cxx | 24 +++++++++++++++++++ panda/src/android/config_android.h | 4 ++++ .../androiddisplay/androidGraphicsWindow.cxx | 6 +++++ 3 files changed, 34 insertions(+) diff --git a/panda/src/android/config_android.cxx b/panda/src/android/config_android.cxx index 083cd7c192..760cf8f51c 100644 --- a/panda/src/android/config_android.cxx +++ b/panda/src/android/config_android.cxx @@ -28,6 +28,9 @@ jmethodID jni_PandaActivity_createBitmap; jmethodID jni_PandaActivity_compressBitmap; jmethodID jni_PandaActivity_showToast; +jclass jni_Activity; +jmethodID jni_Activity_setTitle; + jclass jni_BitmapFactory_Options; jfieldID jni_BitmapFactory_Options_outWidth; jfieldID jni_BitmapFactory_Options_outHeight; @@ -80,6 +83,11 @@ jint JNI_OnLoad(JavaVM *jvm, void *reserved) { jni_PandaActivity_showToast = env->GetMethodID(jni_PandaActivity, "showToast", "(Ljava/lang/String;I)V"); + jni_Activity = env->FindClass("android/app/Activity"); + jni_Activity = (jclass) env->NewGlobalRef(jni_Activity); + jni_Activity_setTitle = env->GetMethodID(jni_Activity, + "setTitle", "(Ljava/lang/CharSequence;)V"); + jni_BitmapFactory_Options = env->FindClass("android/graphics/BitmapFactory$Options"); jni_BitmapFactory_Options = (jclass) env->NewGlobalRef(jni_BitmapFactory_Options); @@ -118,6 +126,7 @@ void JNI_OnUnload(JavaVM *jvm, void *reserved) { nassertv(env != nullptr); env->DeleteGlobalRef(jni_PandaActivity); + env->DeleteGlobalRef(jni_Activity); env->DeleteGlobalRef(jni_BitmapFactory_Options); // These will no longer work without JNI, so unregister them. @@ -135,6 +144,21 @@ void JNI_OnUnload(JavaVM *jvm, void *reserved) { } } +/** + * Sets the window title of the activity. + */ +void android_set_title(ANativeActivity *activity, const std::string &title) { + nassertv(jni_Activity_setTitle); + + Thread *thread = Thread::get_current_thread(); + JNIEnv *env = thread->get_jni_env(); + nassertv(env != nullptr); + + jstring jmsg = env->NewStringUTF(title.c_str()); + env->CallVoidMethod(activity->clazz, jni_Activity_setTitle, jmsg); + env->DeleteLocalRef(jmsg); +} + /** * Shows a toast notification at the bottom of the activity. The duration * should be 0 for short and 1 for long. diff --git a/panda/src/android/config_android.h b/panda/src/android/config_android.h index 4935db0718..71d42af3f3 100644 --- a/panda/src/android/config_android.h +++ b/panda/src/android/config_android.h @@ -35,10 +35,14 @@ extern jmethodID jni_PandaActivity_createBitmap; extern jmethodID jni_PandaActivity_compressBitmap; extern jmethodID jni_PandaActivity_showToast; +extern jclass jni_Activity; +extern jmethodID jni_Activity_setTitle; + extern jclass jni_BitmapFactory_Options; extern jfieldID jni_BitmapFactory_Options_outWidth; extern jfieldID jni_BitmapFactory_Options_outHeight; +EXPORT_CLASS void android_set_title(ANativeActivity *activity, const std::string &title); EXPORT_CLASS void android_show_toast(ANativeActivity *activity, const std::string &message, int duration); #endif diff --git a/panda/src/androiddisplay/androidGraphicsWindow.cxx b/panda/src/androiddisplay/androidGraphicsWindow.cxx index 1cc59d8298..e88d6dae6a 100644 --- a/panda/src/androiddisplay/androidGraphicsWindow.cxx +++ b/panda/src/androiddisplay/androidGraphicsWindow.cxx @@ -15,6 +15,7 @@ #include "androidGraphicsStateGuardian.h" #include "config_androiddisplay.h" #include "androidGraphicsPipe.h" +#include "config_android.h" #include "graphicsPipe.h" #include "keyboardButton.h" @@ -232,6 +233,11 @@ set_properties_now(WindowProperties &properties) { _properties.set_fullscreen(properties.get_fullscreen()); properties.clear_fullscreen(); } + + if (properties.has_title()) { + android_set_title(_app->activity, properties.get_title()); + properties.clear_title(); + } } /** From 4763ddba2163fc670f105717a5efe7ba08a76ca9 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 9 Nov 2025 12:07:43 +0100 Subject: [PATCH 176/291] dist: Neatly indent AndroidManifest.xml --- direct/src/dist/commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 51a7face50..64454f0e33 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -900,6 +900,8 @@ class build_apps(setuptools.Command): ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LEANBACK_LAUNCHER') tree = ET.ElementTree(manifest) + if sys.version_info >= (3, 9): + ET.indent(tree) with open(path, 'wb') as fh: tree.write(fh, encoding='utf-8', xml_declaration=True) From 3d1b897c31f477315e8604d18d92061f5e558d38 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 9 Nov 2025 13:46:45 +0100 Subject: [PATCH 177/291] android: Work on supporting extractNativeLibs:false, separate blob The blob is now stored in a raw resource, so no longer duplicated between each architecture Setting extractNativeLibs:false doesn't yet work, the remaining step is somehow getting Python to load extension modules that are still part of the apk --- direct/src/dist/FreezeTool.py | 59 +++++++++++++--------- direct/src/dist/_android.py | 13 ++++- direct/src/dist/commands.py | 24 +++++++-- direct/src/dist/installers.py | 52 +++++++++++++++---- panda/src/android/PandaActivity.java | 42 +++++++++++++++ panda/src/android/jni_PandaActivity.cxx | 36 +++++++++++++ panda/src/android/p3android_composite1.cxx | 1 + panda/src/express/zipArchive.cxx | 47 +++++++++++------ panda/src/express/zipArchive.h | 12 +++-- pandatool/src/deploy-stub/android_main.cxx | 45 ++++++++++++----- 10 files changed, 258 insertions(+), 73 deletions(-) create mode 100644 panda/src/android/jni_PandaActivity.cxx diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index a56df6f951..0d6e4a0ee1 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -1835,7 +1835,8 @@ class Freezer: return target def generateRuntimeFromStub(self, target, stub_file, use_console, fields={}, - log_append=False, log_filename_strftime=False): + log_append=False, log_filename_strftime=False, + blob_path=None): self.__replacePaths() # We must have a __main__ module to make an exe file. @@ -1983,29 +1984,33 @@ class Freezer: pad = (blob_align - (blob_size & (blob_align - 1))) blob_size += pad - # TODO: Support creating custom sections in universal binaries. - append_blob = True - if self.platform.startswith('macosx') and len(bitnesses) == 1: - # If our deploy-stub has a __PANDA segment, we know we're meant to - # put our blob there rather than attach it to the end. - load_commands = self._parse_macho_load_commands(stub_data) - if b'__PANDA' in load_commands.keys(): - append_blob = False - - if self.platform.startswith("macosx") and not append_blob: - # Take this time to shift any Mach-O structures around to fit our - # blob. We don't need to worry about aligning the offset since the - # compiler already took care of that when creating the segment. - blob_offset = self._shift_macho_structures(stub_data, load_commands, blob_size) + if blob_path is not None: + # We'll be writing the blob to a separate location. + blob_offset = 0 else: - # Add padding before the blob if necessary. - blob_offset = len(stub_data) - if (blob_offset & (blob_align - 1)) != 0: - pad = (blob_align - (blob_offset & (blob_align - 1))) - stub_data += (b'\0' * pad) - blob_offset += pad - assert (blob_offset % blob_align) == 0 - assert blob_offset == len(stub_data) + # TODO: Support creating custom sections in universal binaries. + append_blob = True + if self.platform.startswith('macosx') and len(bitnesses) == 1: + # If our deploy-stub has a __PANDA segment, we know we're meant to + # put our blob there rather than attach it to the end. + load_commands = self._parse_macho_load_commands(stub_data) + if b'__PANDA' in load_commands.keys(): + append_blob = False + + if self.platform.startswith("macosx") and not append_blob: + # Take this time to shift any Mach-O structures around to fit our + # blob. We don't need to worry about aligning the offset since the + # compiler already took care of that when creating the segment. + blob_offset = self._shift_macho_structures(stub_data, load_commands, blob_size) + else: + # Add padding before the blob if necessary. + blob_offset = len(stub_data) + if (blob_offset & (blob_align - 1)) != 0: + pad = (blob_align - (blob_offset & (blob_align - 1))) + stub_data += (b'\0' * pad) + blob_offset += pad + assert (blob_offset % blob_align) == 0 + assert blob_offset == len(stub_data) # Calculate the offsets for the variables. These are pointers, # relative to the beginning of the blob. @@ -2091,7 +2096,9 @@ class Freezer: blob += struct.pack('= 31 and f'{android}exported' not in activity.attrib: raise RuntimeError(" element must have android:exported attribute when targeting Android API 31+") - def build_binaries(self, platform, binary_dir, data_dir=None): + def build_binaries(self, platform, binary_dir, data_dir=None, blob_dir=None): """ Builds the binary data for the given platform. """ use_wheels = True @@ -1133,6 +1140,7 @@ class build_apps(setuptools.Command): stub_name = 'deploy-stub' target_name = appname + appname_sane = appname if platform.startswith('win') or 'macosx' in platform: if not use_console: stub_name = 'deploy-stubw' @@ -1172,6 +1180,14 @@ class build_apps(setuptools.Command): if not self.log_filename or '%' not in self.log_filename: use_strftime = False + blob_path = None + if blob_dir is not None: + if platform.startswith('android'): + # Not really a .so file, but it forces bundletool to align it + blob_path = os.path.join(blob_dir, appname_sane + '.so') + else: + blob_path = os.path.join(blob_dir, appname_sane) + target_path = os.path.join(binary_dir, target_name) freezer.generateRuntimeFromStub(target_path, stub_file, use_console, { 'prc_data': prcexport if self.embed_prc_data else None, @@ -1185,7 +1201,7 @@ class build_apps(setuptools.Command): 'prc_executable_args_envvar': None, 'main_dir': None, 'log_filename': self.expand_path(self.log_filename, platform), - }, self.log_append, use_strftime) + }, self.log_append, use_strftime, blob_path) stub_file.close() if temp_file: diff --git a/direct/src/dist/installers.py b/direct/src/dist/installers.py index 1e1ee5d8ea..85fc3a7335 100644 --- a/direct/src/dist/installers.py +++ b/direct/src/dist/installers.py @@ -207,7 +207,7 @@ def create_aab(command, basename, build_dir): and use it to convert an .aab into an .apk. """ - from ._android import AndroidManifest, AbiAlias, BundleConfig, NativeLibraries, ResourceTable + from ._android import AndroidManifest, AbiAlias, BundleConfig, NativeLibraries, ResourceTable, UncompressNativeLibraries bundle_fn = p3d.Filename.from_os_specific(command.dist_dir) / (basename + '.aab') build_dir_fn = p3d.Filename.from_os_specific(build_dir) @@ -230,7 +230,13 @@ def create_aab(command, basename, build_dir): config = BundleConfig() config.bundletool.version = '1.1.0' config.optimizations.splits_config.Clear() - config.optimizations.uncompress_native_libraries.enabled = False + if axml.extract_native_libs: + config.optimizations.uncompress_native_libraries.enabled = False + config.optimizations.uncompress_native_libraries.alignment = \ + UncompressNativeLibraries.PageAlignment.PAGE_ALIGNMENT_16K + else: + config.optimizations.uncompress_native_libraries.enabled = True + config.compression.uncompressed_glob.append('res/raw/**') bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9) resources = ResourceTable() @@ -251,13 +257,25 @@ def create_aab(command, basename, build_dir): entry.entry_id.id = entry_id entry.name = res_name - for density, tag in (120, 'ldpi'), (160, 'mdpi'), (240, 'hdpi'), (320, 'xhdpi'), (480, 'xxhdpi'), (640, 'xxxhdpi'): - path = f'res/mipmap-{tag}-v4/{res_name}.png' - if (build_dir_fn / path).exists(): - bundle.add_subfile('base/' + path, build_dir_fn / path, 0) - config_value = entry.config_value.add() - config_value.config.density = density - config_value.value.item.file.path = path + if type_name == 'raw': + path = f'res/raw/{res_name}' + if not (build_dir_fn / path).exists(): + command.announce( + f'\tRaw resource {path} was not found on disk', distutils.log.ERROR) + return + + # These are aligned to page size for mmap. + bundle.add_subfile('base/' + path, build_dir_fn / path, 0, 16384) + config_value = entry.config_value.add() + config_value.value.item.file.path = path + else: + for density, tag in (120, 'ldpi'), (160, 'mdpi'), (240, 'hdpi'), (320, 'xhdpi'), (480, 'xxhdpi'), (640, 'xxxhdpi'): + path = f'res/mipmap-{tag}-v4/{res_name}.png' + if (build_dir_fn / path).exists(): + bundle.add_subfile('base/' + path, build_dir_fn / path, 0) + config_value = entry.config_value.add() + config_value.config.density = density + config_value.value.item.file.path = path bundle.add_subfile('base/resources.pb', p3d.StringStream(resources.SerializeToString()), 9) @@ -273,13 +291,25 @@ def create_aab(command, basename, build_dir): # Add the classes.dex. bundle.add_subfile('base/dex/classes.dex', build_dir_fn / 'classes.dex', 9) - # Add libraries, compressed. + # Add libraries, compressed, unless extractNativeLibs is false, in which + # case they have to be aligned to page boundaries (16 KiB on 64-bit). + lib_compress = 9 if axml.extract_native_libs else 0 + for abi in os.listdir(os.path.join(build_dir, 'lib')): abi_dir = os.path.join(build_dir, 'lib', abi) + if axml.extract_native_libs: + lib_align = 0 + elif '64' in abi: + lib_align = 16384 + else: + lib_align = 4096 + for lib in os.listdir(abi_dir): if lib.startswith('lib') and lib.endswith('.so'): - bundle.add_subfile(f'base/lib/{abi}/{lib}', build_dir_fn / 'lib' / abi / lib, 9) + bundle.add_subfile(f'base/lib/{abi}/{lib}', + build_dir_fn / 'lib' / abi / lib, + lib_compress, lib_align) # Add assets, compressed. assets_dir = os.path.join(build_dir, 'assets') diff --git a/panda/src/android/PandaActivity.java b/panda/src/android/PandaActivity.java index d9b390373f..560375c90a 100644 --- a/panda/src/android/PandaActivity.java +++ b/panda/src/android/PandaActivity.java @@ -17,7 +17,9 @@ import android.app.NativeActivity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; import android.net.Uri; +import android.os.ParcelFileDescriptor; import android.widget.Toast; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -25,6 +27,8 @@ import dalvik.system.BaseDexClassLoader; import org.panda3d.android.NativeIStream; import org.panda3d.android.NativeOStream; +import android.util.Log; + /** * The entry point for a Panda-based activity. Loads the Panda libraries and * also provides some utility functions. @@ -77,6 +81,34 @@ public class PandaActivity extends NativeActivity { return Thread.currentThread().getName(); } + /** + * Maps the blob to memory and returns the pointer. + */ + public long mapBlobFromResource() { + int resourceId = 0; + try { + ActivityInfo ai = getPackageManager().getActivityInfo( + getIntent().getComponent(), PackageManager.GET_META_DATA); + if (ai.metaData == null) { + Log.e("Panda3D", "Failed to get activity metadata"); + return 0; + } + resourceId = ai.metaData.getInt("org.panda3d.android.BLOB_RESOURCE"); + if (resourceId == 0) { + return 0; + } + + AssetFileDescriptor afd = getResources().openRawResourceFd(resourceId); + ParcelFileDescriptor pfd = afd.getParcelFileDescriptor(); + long off = afd.getStartOffset(); + long len = afd.getLength(); + return nativeMmap(pfd.getFd(), off, len); + } catch (Exception e) { + Log.e("Panda3D", "Received exception while trying to map blob: " + e); + return 0; + } + } + /** * Returns the path to the main native library. */ @@ -97,6 +129,14 @@ public class PandaActivity extends NativeActivity { return classLoader.findLibrary(libname); } + /** + * Returns the path to some other native library. + */ + public String findLibraryPath(String libname) { + BaseDexClassLoader classLoader = (BaseDexClassLoader)getClassLoader(); + return classLoader.findLibrary(libname); + } + public String getIntentDataPath() { Intent intent = getIntent(); Uri data = intent.getData(); @@ -139,4 +179,6 @@ public class PandaActivity extends NativeActivity { // Contains our JNI calls. System.loadLibrary("p3android"); } + + private static native long nativeMmap(int fd, long off, long len); } diff --git a/panda/src/android/jni_PandaActivity.cxx b/panda/src/android/jni_PandaActivity.cxx new file mode 100644 index 0000000000..9ff91a1b26 --- /dev/null +++ b/panda/src/android/jni_PandaActivity.cxx @@ -0,0 +1,36 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file jni_PandaActivity.cxx + * @author rdb + * @date 2025-11-09 + */ + +#include +#include +#include + +#if __GNUC__ >= 4 +#define EXPORT_JNI extern "C" __attribute__((visibility("default"))) +#else +#define EXPORT_JNI extern "C" +#endif + +/** + * + */ +EXPORT_JNI jlong +Java_org_panda3d_android_PandaActivity_nativeMmap(JNIEnv* env, jclass, jint fd, jlong off, jlong len) { + // Align the offset down to the page size boundary. + size_t page_size = getpagesize(); + off_t aligned = off & ~((off_t)page_size - 1); + size_t delta = (size_t)(off - aligned); + + void *ptr = mmap(nullptr, (size_t)len, PROT_READ, MAP_PRIVATE, fd, aligned); + return (jlong)((uintptr_t)ptr + delta); +} diff --git a/panda/src/android/p3android_composite1.cxx b/panda/src/android/p3android_composite1.cxx index ea14d2b311..5a9154224e 100644 --- a/panda/src/android/p3android_composite1.cxx +++ b/panda/src/android/p3android_composite1.cxx @@ -1,6 +1,7 @@ #include "config_android.cxx" #include "jni_NativeIStream.cxx" #include "jni_NativeOStream.cxx" +#include "jni_PandaActivity.cxx" #include "pnmFileTypeAndroid.cxx" #include "pnmFileTypeAndroidReader.cxx" #include "pnmFileTypeAndroidWriter.cxx" diff --git a/panda/src/express/zipArchive.cxx b/panda/src/express/zipArchive.cxx index d34f9c5b41..9008143159 100644 --- a/panda/src/express/zipArchive.cxx +++ b/panda/src/express/zipArchive.cxx @@ -317,7 +317,7 @@ close() { */ std::string ZipArchive:: add_subfile(const std::string &subfile_name, const Filename &filename, - int compression_level) { + int compression_level, size_t data_alignment) { nassertr(is_write_valid(), std::string()); #ifndef HAVE_ZLIB @@ -337,7 +337,7 @@ add_subfile(const std::string &subfile_name, const Filename &filename, return std::string(); } - std::string name = add_subfile(subfile_name, in, compression_level); + std::string name = add_subfile(subfile_name, in, compression_level, data_alignment); vfs->close_read_file(in); return name; } @@ -356,7 +356,7 @@ add_subfile(const std::string &subfile_name, const Filename &filename, */ std::string ZipArchive:: add_subfile(const std::string &subfile_name, std::istream *subfile_data, - int compression_level) { + int compression_level, size_t data_alignment) { nassertr(is_write_valid(), string()); #ifndef HAVE_ZLIB @@ -367,14 +367,14 @@ add_subfile(const std::string &subfile_name, std::istream *subfile_data, std::string name = standardize_subfile_name(subfile_name); if (!name.empty()) { - Subfile *subfile = new Subfile(subfile_name, compression_level); + Subfile *subfile = new Subfile(subfile_name, compression_level, data_alignment); // Write it straight away, overwriting the index at the end of the file. // This index will be rewritten at the next call to flush() or close(). std::streampos fpos = _index_start; _write->seekp(fpos); - if (!subfile->write_header(*_write, fpos)) { + if (!subfile->write_header(*_write, fpos, data_alignment)) { delete subfile; return ""; } @@ -406,7 +406,7 @@ add_subfile(const std::string &subfile_name, std::istream *subfile_data, */ string ZipArchive:: update_subfile(const std::string &subfile_name, const Filename &filename, - int compression_level) { + int compression_level, size_t data_alignment) { nassertr(is_write_valid(), string()); #ifndef HAVE_ZLIB @@ -431,7 +431,7 @@ update_subfile(const std::string &subfile_name, const Filename &filename, // The subfile does not already exist or it is different from the source // file. Add the new source file. - Subfile *subfile = new Subfile(name, compression_level); + Subfile *subfile = new Subfile(name, compression_level, data_alignment); add_new_subfile(subfile, compression_level); } @@ -764,7 +764,7 @@ repack() { // the checksum and sizes. subfile->_flags &= ~SF_data_descriptor; - if (!subfile->write_header(temp, fpos)) { + if (!subfile->write_header(temp, fpos, subfile->_data_alignment)) { success = false; continue; } @@ -1669,10 +1669,11 @@ write_index(std::ostream &write, std::streampos &fpos) { * Creates a new subfile record. */ ZipArchive::Subfile:: -Subfile(const std::string &name, int compression_level) : +Subfile(const std::string &name, int compression_level, size_t data_alignment) : _name(name), _timestamp(dos_epoch), - _compression_method((compression_level > 0) ? CM_deflate : CM_store) + _compression_method((compression_level > 0) ? CM_deflate : CM_store), + _data_alignment(data_alignment) { // If the name contains any non-ASCII characters, we set the UTF-8 flag. for (char c : name) { @@ -2096,7 +2097,7 @@ write_index(std::ostream &write, streampos &fpos) { * than the actual size of the subfile). */ bool ZipArchive::Subfile:: -write_header(std::ostream &write, std::streampos &fpos) { +write_header(std::ostream &write, std::streampos &fpos, size_t data_alignment) { nassertr(write.tellp() == fpos, false); std::string encoded_name; @@ -2109,13 +2110,29 @@ write_header(std::ostream &write, std::streampos &fpos) { std::streamoff header_size = 30 + encoded_name.size(); StreamWriter writer(write); - int modulo = (fpos + header_size) % 4; - if (!is_compressed() && modulo != 0) { + if (!is_compressed()) { // Align uncompressed files to 4-byte boundary. We don't really need to do // this, but it's needed when producing .apk files, and it doesn't really // cause harm to do it in other cases as well. - writer.pad_bytes(4 - modulo); - fpos += (4 - modulo); + if (data_alignment < 4) { + data_alignment = 4; + } + else if ((data_alignment % 4) != 0) { + data_alignment *= 2; + if ((data_alignment % 4) != 0) { + data_alignment *= 2; + } + } + } + + if (data_alignment > 0) { + // The data follows the header directly, so the actual padding has to be + // inserted before the header. + int modulo = (fpos + header_size) % data_alignment; + if (modulo != 0) { + writer.pad_bytes(data_alignment - modulo); + fpos += (data_alignment - modulo); + } } _header_start = fpos; diff --git a/panda/src/express/zipArchive.h b/panda/src/express/zipArchive.h index 8cfc49ac94..23a17594c6 100644 --- a/panda/src/express/zipArchive.h +++ b/panda/src/express/zipArchive.h @@ -66,11 +66,11 @@ PUBLISHED: INLINE bool get_record_timestamp() const; std::string add_subfile(const std::string &subfile_name, const Filename &filename, - int compression_level); + int compression_level, size_t data_alignment=0); std::string add_subfile(const std::string &subfile_name, std::istream *subfile_data, - int compression_level); + int compression_level, size_t data_alignment=0); std::string update_subfile(const std::string &subfile_name, const Filename &filename, - int compression_level); + int compression_level, size_t data_alignment=0); #ifdef HAVE_OPENSSL bool add_jar_signature(const Filename &certificate, const Filename &pkey, @@ -152,7 +152,8 @@ private: class Subfile { public: Subfile() = default; - Subfile(const std::string &name, int compression_level); + Subfile(const std::string &name, int compression_level, + size_t data_alignment=0); INLINE bool operator < (const Subfile &other) const; @@ -160,7 +161,7 @@ private: bool read_header(std::istream &read); bool verify_data(std::istream &read); bool write_index(std::ostream &write, std::streampos &fpos); - bool write_header(std::ostream &write, std::streampos &fpos); + bool write_header(std::ostream &write, std::streampos &fpos, size_t data_alignment=0); bool write_data(std::ostream &write, std::istream *read, std::streampos &fpos, int compression_level); INLINE bool is_compressed() const; @@ -180,6 +181,7 @@ private: std::string _comment; int _flags = SF_data_descriptor; CompressionMethod _compression_method = CM_store; + size_t _data_alignment = 0; }; void add_new_subfile(Subfile *subfile, int compression_level); diff --git a/pandatool/src/deploy-stub/android_main.cxx b/pandatool/src/deploy-stub/android_main.cxx index 4e1bb7c260..082fadf463 100644 --- a/pandatool/src/deploy-stub/android_main.cxx +++ b/pandatool/src/deploy-stub/android_main.cxx @@ -61,7 +61,7 @@ static void *map_blob(const char *path, off_t offset, size_t size) { FILE *runtime = fopen(path, "rb"); assert(runtime != NULL); - void *blob = (void *)mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fileno(runtime), offset); + void *blob = (void *)mmap(0, size, PROT_READ, MAP_PRIVATE, fileno(runtime), offset); assert(blob != MAP_FAILED); fclose(runtime); @@ -181,10 +181,17 @@ void android_main(struct android_app *app) { android_cat.info() << "Path to native library: " << lib_path << "\n"; ExecutionEnvironment::set_binary_name(lib_path); - // Map the blob to memory - void *blob = map_blob(lib_path, (off_t)blobinfo.blob_offset, (size_t)blobinfo.blob_size); + // Nowadays we store the blob in a raw resource. + methodID = env->GetMethodID(activity_class, "mapBlobFromResource", "()J"); + assert(methodID != nullptr); + void *blob = (void *)env->CallLongMethod(activity->clazz, methodID); + + if (blob == nullptr) { + // Try the old method otherwise. + blob = map_blob(lib_path, (off_t)blobinfo.blob_offset, (size_t)blobinfo.blob_size); + } env->ReleaseStringUTFChars(lib_path_jstr, lib_path); - assert(blob != NULL); + assert(blob != nullptr); assert(blobinfo.num_pointers <= MAX_NUM_POINTERS); for (uint32_t i = 0; i < blobinfo.num_pointers; ++i) { @@ -225,17 +232,28 @@ void android_main(struct android_app *app) { get_model_path().append_directory(asset_dir); // Offset the pointers in the module table using the base mmap address. - struct _frozen *moddef = (struct _frozen *)blobinfo.pointers[0]; - while (moddef->name) { - moddef->name = (char *)((uintptr_t)moddef->name + (uintptr_t)blob); - if (moddef->code != nullptr) { - moddef->code = (unsigned char *)((uintptr_t)moddef->code + (uintptr_t)blob); - } - //__android_log_print(ANDROID_LOG_DEBUG, "Panda3D", "MOD: %s %p %d\n", moddef->name, (void*)moddef->code, moddef->size); - moddef++; + // We did a read-only mmap, so we have to create a copy of this table. + // First count how many modules there are. + struct _frozen *src_moddef = (struct _frozen *)blobinfo.pointers[0]; + size_t num_modules = 0; + while (src_moddef->name) { + num_modules++; + src_moddef++; } - PyImport_FrozenModules = (struct _frozen *)blobinfo.pointers[0]; + struct _frozen *new_modules = (struct _frozen *)malloc(sizeof(struct _frozen) * (num_modules + 1)); + memcpy(new_modules, blobinfo.pointers[0], sizeof(struct _frozen) * (num_modules + 1)); + + struct _frozen *dst_moddef = new_modules; + while (dst_moddef->name) { + dst_moddef->name = (char *)((uintptr_t)dst_moddef->name + (uintptr_t)blob); + if (dst_moddef->code != nullptr) { + dst_moddef->code = (unsigned char *)((uintptr_t)dst_moddef->code + (uintptr_t)blob); + } + __android_log_print(ANDROID_LOG_DEBUG, "Panda3D", "MOD: %s %p %d\n", dst_moddef->name, (void*)dst_moddef->code, dst_moddef->size); + dst_moddef++; + } + PyImport_FrozenModules = new_modules; PyPreConfig preconfig; PyPreConfig_InitIsolatedConfig(&preconfig); @@ -328,6 +346,7 @@ void android_main(struct android_app *app) { cp_mgr->delete_explicit_page(page); } + free(new_modules); unmap_blob(blob); // Detach the thread before exiting. From 598106c6ddab5c821fe6d7eddc1351ecd73b62f8 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 9 Nov 2025 21:55:04 +0100 Subject: [PATCH 178/291] dist: Show error for mismatching build_apps/wheel --- direct/src/dist/commands.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 6dd3ba693c..241bc6391b 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -1150,6 +1150,17 @@ class build_apps(setuptools.Command): appname_sane = appname.replace(' ', '_') target_name = 'lib' + appname_sane + '.so' + if use_wheels: + dexfile = os.path.join(binary_dir, '..', '..', 'classes.dex') + self.copy(os.path.join(p3dwhlfn, 'deploy_libs', 'classes.dex'), dexfile) + + # Can this wheel load the blob as a raw resource? + with open(dexfile, 'rb') as fh: + supports_blob_resource = b'org.panda3d.android.BLOB_RESOURCE' in fh.read() + + assert supports_blob_resource, \ + "Please use a newer Panda3D wheel to build for Android using this version of build_apps" + if platform.startswith('win'): stub_name += '.exe' target_name += '.exe' @@ -1316,11 +1327,6 @@ class build_apps(setuptools.Command): search_path = get_search_path_for(source_path) self.copy_with_dependencies(source_path, target_path, search_path) - # Copy classes.dex on Android - if use_wheels and platform.startswith('android'): - self.copy(os.path.join(p3dwhlfn, 'deploy_libs', 'classes.dex'), - os.path.join(binary_dir, '..', '..', 'classes.dex')) - # Extract any other data files from dependency packages. if data_dir is None: return From e809c9eb92979cb6cb2a5dde5adcdcf9d2c32f0a Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 9 Nov 2025 21:58:08 +0100 Subject: [PATCH 179/291] android: Complete support for android:extractNativeLibs=false This is the non-legacy way to ship native libraries nowadays, leaving them in the .apk so that there doesn't need to be an extraction step and the app takes up less space. --- direct/src/dist/FreezeTool.py | 2 +- direct/src/dist/commands.py | 12 +++---- direct/src/dist/installers.py | 4 +-- dtool/src/dtoolutil/load_dso.cxx | 10 ++++++ makepanda/makepanda.py | 4 +-- panda/src/android/PandaActivity.java | 2 +- panda/src/android/config_android.cxx | 26 ++++++++++++++ panda/src/android/config_android.h | 1 + pandatool/src/deploy-stub/android_main.cxx | 13 +++---- .../{android_log.c => android_support.cxx} | 34 +++++++++++++++---- 10 files changed, 82 insertions(+), 26 deletions(-) rename pandatool/src/deploy-stub/{android_log.c => android_support.cxx} (50%) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index 0d6e4a0ee1..34f9a1d09f 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -114,7 +114,7 @@ ignoreImports = { 'toml.encoder': ['numpy'], 'py._builtin': ['__builtin__'], - 'site': ['android_log'], + 'site': ['android_support'], } if sys.version_info >= (3, 8): diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 241bc6391b..d0b4af8814 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -192,14 +192,14 @@ SITE_PY_ANDROID = """ # module. import sys, os from importlib import _bootstrap, _bootstrap_external +from android_support import log_write as android_log_write +from android_support import find_library class AndroidExtensionFinder: @classmethod def find_spec(cls, fullname, path=None, target=None): - soname = 'libpy.' + fullname + '.so' - path = os.path.join(sys.platlibdir, soname) - - if os.path.exists(path): + path = find_library('py.' + fullname) + if path: loader = _bootstrap_external.ExtensionFileLoader(fullname, path) return _bootstrap.ModuleSpec(fullname, loader, origin=path) @@ -210,8 +210,6 @@ sys.meta_path.append(AndroidExtensionFinder) from _frozen_importlib import _imp, FrozenImporter from io import RawIOBase, TextIOWrapper -from android_log import write as android_log_write - sys.frozen = True @@ -869,7 +867,7 @@ class build_apps(setuptools.Command): if category: application.set('android:appCategory', category) application.set('android:debuggable', ('false', 'true')[self.android_debuggable]) - application.set('android:extractNativeLibs', 'true') + application.set('android:extractNativeLibs', 'false') application.set('android:hardwareAccelerated', 'true') app_icon = self.icon_objects.get('*', self.icon_objects.get(self.macos_main_app)) diff --git a/direct/src/dist/installers.py b/direct/src/dist/installers.py index 85fc3a7335..c5b1475190 100644 --- a/direct/src/dist/installers.py +++ b/direct/src/dist/installers.py @@ -232,10 +232,10 @@ def create_aab(command, basename, build_dir): config.optimizations.splits_config.Clear() if axml.extract_native_libs: config.optimizations.uncompress_native_libraries.enabled = False - config.optimizations.uncompress_native_libraries.alignment = \ - UncompressNativeLibraries.PageAlignment.PAGE_ALIGNMENT_16K else: config.optimizations.uncompress_native_libraries.enabled = True + config.optimizations.uncompress_native_libraries.alignment = \ + UncompressNativeLibraries.PageAlignment.PAGE_ALIGNMENT_16K config.compression.uncompressed_glob.append('res/raw/**') bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9) diff --git a/dtool/src/dtoolutil/load_dso.cxx b/dtool/src/dtoolutil/load_dso.cxx index 95650b88c2..ea212009a7 100644 --- a/dtool/src/dtoolutil/load_dso.cxx +++ b/dtool/src/dtoolutil/load_dso.cxx @@ -19,11 +19,15 @@ using std::string; static Filename resolve_dso(const DSearchPath &path, const Filename &filename) { if (filename.is_local()) { if ((path.get_num_directories()==1)&&(path.get_directory(0)=="")) { +#ifdef ANDROID + return filename; +#else // This is a special case, meaning to search in the same directory in // which libp3dtool.dll, or the exe, was started from. Filename dtoolpath = ExecutionEnvironment::get_dtool_name(); DSearchPath spath(dtoolpath.get_dirname()); return spath.find_file(filename); +#endif } else { return path.find_file(filename); } @@ -119,7 +123,13 @@ get_dso_symbol(void *handle, const string &name) { void * load_dso(const DSearchPath &path, const Filename &filename) { Filename abspath = resolve_dso(path, filename); +#ifdef ANDROID + // We just try to load it on Android, because we can't verify right now + // whether it might just be an unextracted library. + if (abspath.empty()) { +#else if (!abspath.is_regular_file()) { +#endif // Make sure the error flag is cleared, to prevent a subsequent call to // load_dso_error() from returning a previously stored error. dlerror(); diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 9c1a14f081..ad0b72777c 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -5967,10 +5967,10 @@ if PkgSkip("PYTHON") == 0: TargetAdd('classes.dex', input='org/jnius/NativeInvocationHandler.class') PyTargetAdd('deploy-stubw_android_main.obj', opts=OPTS, input='android_main.cxx') - PyTargetAdd('deploy-stubw_android_log.obj', opts=OPTS, input='android_log.c') + PyTargetAdd('deploy-stubw_android_support.obj', opts=OPTS, input='android_support.cxx') PyTargetAdd('libdeploy-stubw.dll', input='android_native_app_glue.obj') PyTargetAdd('libdeploy-stubw.dll', input='deploy-stubw_android_main.obj') - PyTargetAdd('libdeploy-stubw.dll', input='deploy-stubw_android_log.obj') + PyTargetAdd('libdeploy-stubw.dll', input='deploy-stubw_android_support.obj') PyTargetAdd('libdeploy-stubw.dll', input=COMMON_PANDA_LIBS) PyTargetAdd('libdeploy-stubw.dll', input='libp3android.dll') PyTargetAdd('libdeploy-stubw.dll', opts=['DEPLOYSTUB', 'ANDROID']) diff --git a/panda/src/android/PandaActivity.java b/panda/src/android/PandaActivity.java index 560375c90a..866c5bfc34 100644 --- a/panda/src/android/PandaActivity.java +++ b/panda/src/android/PandaActivity.java @@ -132,7 +132,7 @@ public class PandaActivity extends NativeActivity { /** * Returns the path to some other native library. */ - public String findLibraryPath(String libname) { + public String findLibrary(String libname) { BaseDexClassLoader classLoader = (BaseDexClassLoader)getClassLoader(); return classLoader.findLibrary(libname); } diff --git a/panda/src/android/config_android.cxx b/panda/src/android/config_android.cxx index 760cf8f51c..88ddba3c50 100644 --- a/panda/src/android/config_android.cxx +++ b/panda/src/android/config_android.cxx @@ -27,6 +27,7 @@ jmethodID jni_PandaActivity_readBitmap; jmethodID jni_PandaActivity_createBitmap; jmethodID jni_PandaActivity_compressBitmap; jmethodID jni_PandaActivity_showToast; +jmethodID jni_PandaActivity_findLibrary; jclass jni_Activity; jmethodID jni_Activity_setTitle; @@ -83,6 +84,9 @@ jint JNI_OnLoad(JavaVM *jvm, void *reserved) { jni_PandaActivity_showToast = env->GetMethodID(jni_PandaActivity, "showToast", "(Ljava/lang/String;I)V"); + jni_PandaActivity_findLibrary = env->GetMethodID(jni_PandaActivity, + "findLibrary", "(Ljava/lang/String;)Ljava/lang/String;"); + jni_Activity = env->FindClass("android/app/Activity"); jni_Activity = (jclass) env->NewGlobalRef(jni_Activity); jni_Activity_setTitle = env->GetMethodID(jni_Activity, @@ -144,6 +148,28 @@ void JNI_OnUnload(JavaVM *jvm, void *reserved) { } } +/** + * + */ +Filename android_find_library(ANativeActivity *activity, const std::string &lib) { + Thread *thread = Thread::get_current_thread(); + JNIEnv *env = thread->get_jni_env(); + nassertr(env != nullptr, Filename()); + + jstring jlib = env->NewStringUTF(lib.c_str()); + jstring jresult = (jstring)env->CallObjectMethod(activity->clazz, jni_PandaActivity_findLibrary, jlib); + env->DeleteLocalRef(jlib); + + Filename result; + if (jresult != nullptr) { + const char *c_str = env->GetStringUTFChars(jresult, nullptr); + result = c_str; + env->ReleaseStringUTFChars(jresult, c_str); + } + + return result; +} + /** * Sets the window title of the activity. */ diff --git a/panda/src/android/config_android.h b/panda/src/android/config_android.h index 71d42af3f3..286e517a39 100644 --- a/panda/src/android/config_android.h +++ b/panda/src/android/config_android.h @@ -42,6 +42,7 @@ extern jclass jni_BitmapFactory_Options; extern jfieldID jni_BitmapFactory_Options_outWidth; extern jfieldID jni_BitmapFactory_Options_outHeight; +EXPORT_CLASS Filename android_find_library(ANativeActivity *activity, const std::string &lib); EXPORT_CLASS void android_set_title(ANativeActivity *activity, const std::string &title); EXPORT_CLASS void android_show_toast(ANativeActivity *activity, const std::string &message, int duration); diff --git a/pandatool/src/deploy-stub/android_main.cxx b/pandatool/src/deploy-stub/android_main.cxx index 082fadf463..685062c5ef 100644 --- a/pandatool/src/deploy-stub/android_main.cxx +++ b/pandatool/src/deploy-stub/android_main.cxx @@ -50,8 +50,8 @@ extern "C" { } blobinfo = {(uint64_t)-1}; } -// Defined in android_log.c -extern "C" PyObject *PyInit_android_log(); +// Defined in android_support.cxx +extern "C" PyObject *PyInit_android_support(); /** * Maps the binary blob at the given memory address to memory, and returns the @@ -250,7 +250,7 @@ void android_main(struct android_app *app) { if (dst_moddef->code != nullptr) { dst_moddef->code = (unsigned char *)((uintptr_t)dst_moddef->code + (uintptr_t)blob); } - __android_log_print(ANDROID_LOG_DEBUG, "Panda3D", "MOD: %s %p %d\n", dst_moddef->name, (void*)dst_moddef->code, dst_moddef->size); + //__android_log_print(ANDROID_LOG_DEBUG, "Panda3D", "MOD: %s %p %d\n", dst_moddef->name, (void*)dst_moddef->code, dst_moddef->size); dst_moddef++; } PyImport_FrozenModules = new_modules; @@ -265,10 +265,10 @@ void android_main(struct android_app *app) { return; } - // Register the android_log module. - if (PyImport_AppendInittab("android_log", &PyInit_android_log) < 0) { + // Register the android_support module. + if (PyImport_AppendInittab("android_support", &PyInit_android_support) < 0) { android_cat.error() - << "Failed to register android_log module.\n"; + << "Failed to register android_support module.\n"; env->ReleaseStringUTFChars(libdir_jstr, libdir); return; } @@ -279,6 +279,7 @@ void android_main(struct android_app *app) { config.buffered_stdio = 0; config.configure_c_stdio = 0; config.write_bytecode = 0; + config.module_search_paths_set = 1; // Leave sys.path empty PyConfig_SetBytesString(&config, &config.platlibdir, libdir); env->ReleaseStringUTFChars(libdir_jstr, libdir); diff --git a/pandatool/src/deploy-stub/android_log.c b/pandatool/src/deploy-stub/android_support.cxx similarity index 50% rename from pandatool/src/deploy-stub/android_log.c rename to pandatool/src/deploy-stub/android_support.cxx index 83b6090111..d24a8def93 100644 --- a/pandatool/src/deploy-stub/android_log.c +++ b/pandatool/src/deploy-stub/android_support.cxx @@ -6,7 +6,7 @@ * license. You should have received a copy of this license along * with this source code in a file named "LICENSE." * - * @file android_log.c + * @file android_support.c * @author rdb * @date 2021-12-10 */ @@ -17,12 +17,14 @@ #include "Python.h" #include +#include "android_native_app_glue.h" +#include "config_android.h" /** * Writes a message to the Android log. */ static PyObject * -_py_write(PyObject *self, PyObject *args) { +_py_log_write(PyObject *self, PyObject *args) { int prio; char *tag; char *text; @@ -33,14 +35,32 @@ _py_write(PyObject *self, PyObject *args) { return NULL; } +/** + * Returns the path to a library, if it can be found. + */ +static PyObject * +_py_find_library(PyObject *self, PyObject *args) { + char *lib; + if (PyArg_ParseTuple(args, "s", &lib)) { + Filename result = android_find_library(panda_android_app->activity, lib); + if (!result.empty()) { + return PyUnicode_FromStringAndSize(result.c_str(), (Py_ssize_t)result.length()); + } else { + Py_RETURN_NONE; + } + } + return NULL; +} + static PyMethodDef python_simple_funcs[] = { - { "write", &_py_write, METH_VARARGS }, + { "log_write", &_py_log_write, METH_VARARGS }, + { "find_library", &_py_find_library, METH_VARARGS }, { NULL, NULL } }; -static struct PyModuleDef android_log_module = { +static struct PyModuleDef android_support_module = { PyModuleDef_HEAD_INIT, - "android_log", + "android_support", NULL, -1, python_simple_funcs, @@ -48,6 +68,6 @@ static struct PyModuleDef android_log_module = { }; __attribute__((visibility("default"))) -PyObject *PyInit_android_log() { - return PyModule_Create(&android_log_module); +extern "C" PyObject *PyInit_android_support() { + return PyModule_Create(&android_support_module); } From 325727ef2f648d84ee13b310fb0da7760354f909 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 9 Nov 2025 23:21:49 +0100 Subject: [PATCH 180/291] deploy-stub: Add flag for enabling verbose mode in Python --- pandatool/src/deploy-stub/android_main.cxx | 12 ++++++++++++ pandatool/src/deploy-stub/deploy-stub.c | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/pandatool/src/deploy-stub/android_main.cxx b/pandatool/src/deploy-stub/android_main.cxx index 685062c5ef..dc6a3c2317 100644 --- a/pandatool/src/deploy-stub/android_main.cxx +++ b/pandatool/src/deploy-stub/android_main.cxx @@ -32,6 +32,14 @@ // Leave room for future expansion. #define MAX_NUM_POINTERS 24 +/* Stored in the flags field of the blobinfo structure below. */ +enum Flags { + F_log_append = 1, + F_log_filename_strftime = 2, + F_keep_docstrings = 4, + F_python_verbose = 8, +}; + // Define an exposed symbol where we store the offset to the module data. extern "C" { __attribute__((__visibility__("default"), used)) @@ -283,6 +291,10 @@ void android_main(struct android_app *app) { PyConfig_SetBytesString(&config, &config.platlibdir, libdir); env->ReleaseStringUTFChars(libdir_jstr, libdir); + if (blobinfo.flags & F_python_verbose) { + config.verbose = 1; + } + status = Py_InitializeFromConfig(&config); PyConfig_Clear(&config); if (PyStatus_Exception(status)) { diff --git a/pandatool/src/deploy-stub/deploy-stub.c b/pandatool/src/deploy-stub/deploy-stub.c index 6cd2c2803c..b47615a6b7 100644 --- a/pandatool/src/deploy-stub/deploy-stub.c +++ b/pandatool/src/deploy-stub/deploy-stub.c @@ -35,6 +35,7 @@ enum Flags { F_log_append = 1, F_log_filename_strftime = 2, F_keep_docstrings = 4, + F_python_verbose = 8, }; /* Define an exposed symbol where we store the offset to the module data. */ @@ -416,6 +417,10 @@ int Py_FrozenMain(int argc, char **argv) } #endif + if (blobinfo.flags & F_python_verbose) { + Py_VerboseFlag = 1; + } + #ifndef NDEBUG if ((p = Py_GETENV("PYTHONINSPECT")) && *p != '\0') inspect = 1; From 382078cdafaba3464ec59fb015b552dc245707d4 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 9 Nov 2025 23:23:12 +0100 Subject: [PATCH 181/291] android: Fix strange compilation error on 32-bit Android --- pandatool/src/deploy-stub/android_support.cxx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pandatool/src/deploy-stub/android_support.cxx b/pandatool/src/deploy-stub/android_support.cxx index d24a8def93..bb13166871 100644 --- a/pandatool/src/deploy-stub/android_support.cxx +++ b/pandatool/src/deploy-stub/android_support.cxx @@ -6,19 +6,20 @@ * license. You should have received a copy of this license along * with this source code in a file named "LICENSE." * - * @file android_support.c + * @file android_support.cxx * @author rdb * @date 2021-12-10 */ +#include +#include "android_native_app_glue.h" +#include "config_android.h" + #undef _POSIX_C_SOURCE #undef _XOPEN_SOURCE #define PY_SSIZE_T_CLEAN 1 #include "Python.h" -#include -#include "android_native_app_glue.h" -#include "config_android.h" /** * Writes a message to the Android log. From 72fc5e1f1baeccefdf2e4e01764696e912111ac7 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 9 Nov 2025 23:31:45 +0100 Subject: [PATCH 182/291] android: Remove unused include --- pandatool/src/deploy-stub/android_main.cxx | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandatool/src/deploy-stub/android_main.cxx b/pandatool/src/deploy-stub/android_main.cxx index dc6a3c2317..f77e13512d 100644 --- a/pandatool/src/deploy-stub/android_main.cxx +++ b/pandatool/src/deploy-stub/android_main.cxx @@ -27,8 +27,6 @@ #include #include -#include - // Leave room for future expansion. #define MAX_NUM_POINTERS 24 From 7c7f09241ded69c82c05952287b9387c79895741 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 10 Nov 2025 00:09:26 +0100 Subject: [PATCH 183/291] android: Robustify blob mapping --- panda/src/android/PandaActivity.java | 4 +- panda/src/android/jni_PandaActivity.cxx | 8 ++- pandatool/src/deploy-stub/android_main.cxx | 58 ++++++++++++++++------ 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/panda/src/android/PandaActivity.java b/panda/src/android/PandaActivity.java index 866c5bfc34..e3d6836407 100644 --- a/panda/src/android/PandaActivity.java +++ b/panda/src/android/PandaActivity.java @@ -84,7 +84,7 @@ public class PandaActivity extends NativeActivity { /** * Maps the blob to memory and returns the pointer. */ - public long mapBlobFromResource() { + public long mapBlobFromResource(long offset) { int resourceId = 0; try { ActivityInfo ai = getPackageManager().getActivityInfo( @@ -100,7 +100,7 @@ public class PandaActivity extends NativeActivity { AssetFileDescriptor afd = getResources().openRawResourceFd(resourceId); ParcelFileDescriptor pfd = afd.getParcelFileDescriptor(); - long off = afd.getStartOffset(); + long off = afd.getStartOffset() + offset; long len = afd.getLength(); return nativeMmap(pfd.getFd(), off, len); } catch (Exception e) { diff --git a/panda/src/android/jni_PandaActivity.cxx b/panda/src/android/jni_PandaActivity.cxx index 9ff91a1b26..af1ae1e140 100644 --- a/panda/src/android/jni_PandaActivity.cxx +++ b/panda/src/android/jni_PandaActivity.cxx @@ -31,6 +31,10 @@ Java_org_panda3d_android_PandaActivity_nativeMmap(JNIEnv* env, jclass, jint fd, off_t aligned = off & ~((off_t)page_size - 1); size_t delta = (size_t)(off - aligned); - void *ptr = mmap(nullptr, (size_t)len, PROT_READ, MAP_PRIVATE, fd, aligned); - return (jlong)((uintptr_t)ptr + delta); + void *ptr = mmap(nullptr, (size_t)len + delta, PROT_READ, MAP_PRIVATE, fd, aligned); + if (ptr != MAP_FAILED && ptr != nullptr) { + return (jlong)((uintptr_t)ptr + delta); + } else { + return (jlong)0; + } } diff --git a/pandatool/src/deploy-stub/android_main.cxx b/pandatool/src/deploy-stub/android_main.cxx index f77e13512d..d0b0e7d3b5 100644 --- a/pandatool/src/deploy-stub/android_main.cxx +++ b/pandatool/src/deploy-stub/android_main.cxx @@ -67,11 +67,16 @@ static void *map_blob(const char *path, off_t offset, size_t size) { FILE *runtime = fopen(path, "rb"); assert(runtime != NULL); - void *blob = (void *)mmap(0, size, PROT_READ, MAP_PRIVATE, fileno(runtime), offset); + // Align the offset down to the page size boundary. + size_t page_size = getpagesize(); + off_t aligned = offset & ~((off_t)page_size - 1); + size_t delta = (size_t)(offset - aligned); + + void *blob = (void *)mmap(0, size + delta, PROT_READ, MAP_PRIVATE, fileno(runtime), aligned); assert(blob != MAP_FAILED); fclose(runtime); - return blob; + return (void *)((uintptr_t)blob + delta); } /** @@ -188,9 +193,9 @@ void android_main(struct android_app *app) { ExecutionEnvironment::set_binary_name(lib_path); // Nowadays we store the blob in a raw resource. - methodID = env->GetMethodID(activity_class, "mapBlobFromResource", "()J"); + methodID = env->GetMethodID(activity_class, "mapBlobFromResource", "(J)J"); assert(methodID != nullptr); - void *blob = (void *)env->CallLongMethod(activity->clazz, methodID); + void *blob = (void *)env->CallLongMethod(activity->clazz, methodID, blobinfo.blob_offset); if (blob == nullptr) { // Try the old method otherwise. @@ -199,6 +204,18 @@ void android_main(struct android_app *app) { env->ReleaseStringUTFChars(lib_path_jstr, lib_path); assert(blob != nullptr); + // This should always be aligned, but just in case... + void *blob_copy = nullptr; + if ((((uintptr_t)blob) & (sizeof(void *) - 1)) != 0) { + android_cat.warning() + << "Blob with offset " << blobinfo.blob_offset + << " and size " << blobinfo.blob_size << " is not aligned!\n"; + + blob_copy = malloc(blobinfo.blob_size); + memcpy(blob_copy, blob, blobinfo.blob_size); + blob = blob_copy; + } + assert(blobinfo.num_pointers <= MAX_NUM_POINTERS); for (uint32_t i = 0; i < blobinfo.num_pointers; ++i) { // Only offset if the pointer is non-NULL. Except for the first @@ -241,16 +258,24 @@ void android_main(struct android_app *app) { // We did a read-only mmap, so we have to create a copy of this table. // First count how many modules there are. struct _frozen *src_moddef = (struct _frozen *)blobinfo.pointers[0]; - size_t num_modules = 0; - while (src_moddef->name) { - num_modules++; - src_moddef++; + struct _frozen *dst_moddef; + struct _frozen *new_modules = nullptr; + if (blob_copy != nullptr) { + // We already made a copy, so we can just write to this. + dst_moddef = src_moddef; + } else { + size_t num_modules = 0; + while (src_moddef->name) { + num_modules++; + src_moddef++; + } + + new_modules = (struct _frozen *)malloc(sizeof(struct _frozen) * (num_modules + 1)); + memcpy(new_modules, blobinfo.pointers[0], sizeof(struct _frozen) * (num_modules + 1)); + dst_moddef = new_modules; } + PyImport_FrozenModules = dst_moddef; - struct _frozen *new_modules = (struct _frozen *)malloc(sizeof(struct _frozen) * (num_modules + 1)); - memcpy(new_modules, blobinfo.pointers[0], sizeof(struct _frozen) * (num_modules + 1)); - - struct _frozen *dst_moddef = new_modules; while (dst_moddef->name) { dst_moddef->name = (char *)((uintptr_t)dst_moddef->name + (uintptr_t)blob); if (dst_moddef->code != nullptr) { @@ -259,7 +284,6 @@ void android_main(struct android_app *app) { //__android_log_print(ANDROID_LOG_DEBUG, "Panda3D", "MOD: %s %p %d\n", dst_moddef->name, (void*)dst_moddef->code, dst_moddef->size); dst_moddef++; } - PyImport_FrozenModules = new_modules; PyPreConfig preconfig; PyPreConfig_InitIsolatedConfig(&preconfig); @@ -357,8 +381,14 @@ void android_main(struct android_app *app) { cp_mgr->delete_explicit_page(page); } - free(new_modules); + PyImport_FrozenModules = nullptr; + if (new_modules != nullptr) { + free(new_modules); + } unmap_blob(blob); + if (blob_copy != nullptr) { + free(blob_copy); + } // Detach the thread before exiting. activity->vm->DetachCurrentThread(); From 429af9ae57879a518296c1e126d80668d1eda984 Mon Sep 17 00:00:00 2001 From: Mohamed Yasser Mostafa <129879600+Htrxxxxx@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:37:53 +0200 Subject: [PATCH 184/291] putil: Add method to clear BamCache on disk (#1774) --- panda/src/putil/bamCache.I | 1 + panda/src/putil/bamCache.cxx | 83 ++++++++++++++++++++++++++++++++++++ panda/src/putil/bamCache.h | 4 +- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/panda/src/putil/bamCache.I b/panda/src/putil/bamCache.I index 38a9e45540..5729d6d0fb 100644 --- a/panda/src/putil/bamCache.I +++ b/panda/src/putil/bamCache.I @@ -257,3 +257,4 @@ mark_index_stale() { _index_stale_since = time(nullptr); } } + diff --git a/panda/src/putil/bamCache.cxx b/panda/src/putil/bamCache.cxx index c6fd7d5e88..47ec79a9ce 100644 --- a/panda/src/putil/bamCache.cxx +++ b/panda/src/putil/bamCache.cxx @@ -959,6 +959,89 @@ do_read_record(const Filename &cache_pathname, bool read_data) { return record; } +/** + * Clear the model cache. + * + * Acquires the internal _lock for thread-safety (so callers do NOT need to + * hold the lock). If no cache root is configured (_root.empty()), returns + * immediately. If the cache is marked _read_only, resets only the in-memory + * index and emits a warning message. + * If the VirtualFileSystem global pointer is not available, logs an error, + * resets only the in-memory index, and returns immediately. Otherwise, scans + * the cache root directory and deletes cache files matching the known cache + * extensions (.bam, .txo, .sho). Removes the on-disk index file (if known) + * or the index reference file and clears the in-memory index reference + * contents. Resets the in-memory index to an empty BamCacheIndex. Marks the + * index stale and attempts to flush a new, empty on-disk index so other + * processes will observe the cleared state. + */ +void BamCache:: +clear() { + ReMutexHolder holder(_lock); + + VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr(); + + if (_root.empty()) { + return; + } + + // If the cache is read-only, only reset the in-memory index and log. + if (_read_only) { + util_cat.info() << "BamCache::clear(): cache is read-only; clearing in-memory index only.\n"; + reset_in_memory_index(); + return; + } + + if (vfs == nullptr) { + util_cat.error() << "BamCache::clear(): VFS is not available\n"; + reset_in_memory_index(); + return; + } + + // Delete cache files (.bam, .txo, .sho) in the cache root directory. + PT(VirtualFileList) contents = vfs->scan_directory(_root); + if (contents != nullptr) { + int num_files = contents->get_num_files(); + for (int ci = 0; ci < num_files; ++ci) { + VirtualFile *file = contents->get_file(ci); + Filename filename = file->get_filename(); + const std::string ext = filename.get_extension(); + + if (ext == "bam" || ext == "txo" || ext == "sho") { + Filename pathname(_root, filename); + if (!vfs->delete_file(pathname)) { + util_cat.debug() + << "Could not delete cache file " << pathname << "\n"; + } + } + } + } + + // Remove index files: the index file itself (if known) and the index ref. + Filename index_ref_pathname(_root, Filename("index_name.txt")); + if (!_index_pathname.empty()) { + vfs->delete_file(_index_pathname); + _index_pathname = Filename(); + _index_ref_contents.clear(); + } else { + vfs->delete_file(index_ref_pathname); + } + + reset_in_memory_index(); + + // Try to write an empty index back to disk: mark stale then flush. + mark_index_stale(); + flush_index(); +} + +/* Reset the in-memory cache index to an empty state (caller must hold _lock). */ +void BamCache:: +reset_in_memory_index() { + delete _index; + _index = new BamCacheIndex; + _index_stale_since = 0; +} + /** * Returns the appropriate filename to use for a cache file, given the * fullpath string to the source filename. diff --git a/panda/src/putil/bamCache.h b/panda/src/putil/bamCache.h index 28b2335ff6..28a81f8d31 100644 --- a/panda/src/putil/bamCache.h +++ b/panda/src/putil/bamCache.h @@ -80,6 +80,8 @@ PUBLISHED: void list_index(std::ostream &out, int indent_level = 0) const; + void clear(); + INLINE static BamCache *get_global_ptr(); INLINE static void consider_flush_global_index(); INLINE static void flush_global_index(); @@ -104,7 +106,7 @@ private: void merge_index(BamCacheIndex *new_index); void rebuild_index(); INLINE void mark_index_stale(); - + void reset_in_memory_index(); void add_to_index(const BamCacheRecord *record); void remove_from_index(const Filename &source_filename); From 062e3365fd2a25b10d6b839d374dc84a162f5e20 Mon Sep 17 00:00:00 2001 From: DTM1218 Date: Fri, 11 Jul 2025 23:47:03 -0700 Subject: [PATCH 185/291] directgui: Add default release sounds for buttons --- direct/src/gui/DirectButton.py | 27 +++++++++++++++++++++------ direct/src/gui/DirectCheckBox.py | 1 + direct/src/gui/DirectGuiGlobals.py | 10 +++++++++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/direct/src/gui/DirectButton.py b/direct/src/gui/DirectButton.py index 0ed5523854..c4d3a819d1 100644 --- a/direct/src/gui/DirectButton.py +++ b/direct/src/gui/DirectButton.py @@ -44,6 +44,7 @@ class DirectButton(DirectFrame): # Sounds to be used for button events ('rolloverSound', DGG.getDefaultRolloverSound(), self.setRolloverSound), ('clickSound', DGG.getDefaultClickSound(), self.setClickSound), + ('releaseSound', DGG.getDefaultReleaseSound(), self.setReleaseSound), # Can only be specified at time of widget contruction # Do the text/graphics appear to move when the button is clicked ('pressEffect', 1, DGG.INITOPT), @@ -108,6 +109,13 @@ class DirectButton(DirectFrame): # Pass any extra args to command self['command'](*self['extraArgs']) + def setRolloverSound(self): + rolloverSound = self['rolloverSound'] + if rolloverSound: + self.guiItem.setSound(DGG.ENTER + self.guiId, rolloverSound) + else: + self.guiItem.clearSound(DGG.ENTER + self.guiId) + def setClickSound(self): clickSound = self['clickSound'] # Clear out sounds @@ -122,9 +130,16 @@ class DirectButton(DirectFrame): if DGG.RMB in self['commandButtons']: self.guiItem.setSound(DGG.B3PRESS + self.guiId, clickSound) - def setRolloverSound(self): - rolloverSound = self['rolloverSound'] - if rolloverSound: - self.guiItem.setSound(DGG.ENTER + self.guiId, rolloverSound) - else: - self.guiItem.clearSound(DGG.ENTER + self.guiId) + def setReleaseSound(self): + releaseSound = self['releaseSound'] + # Clear out sounds + self.guiItem.clearSound(DGG.B1RELEASE + self.guiId) + self.guiItem.clearSound(DGG.B2RELEASE + self.guiId) + self.guiItem.clearSound(DGG.B3RELEASE + self.guiId) + if releaseSound: + if DGG.LMB in self['commandButtons']: + self.guiItem.setSound(DGG.B1RELEASE + self.guiId, releaseSound) + if DGG.MMB in self['commandButtons']: + self.guiItem.setSound(DGG.B2RELEASE + self.guiId, releaseSound) + if DGG.RMB in self['commandButtons']: + self.guiItem.setSound(DGG.B3RELEASE + self.guiId, releaseSound) diff --git a/direct/src/gui/DirectCheckBox.py b/direct/src/gui/DirectCheckBox.py index fc0ce5f5ca..d1cff642a8 100755 --- a/direct/src/gui/DirectCheckBox.py +++ b/direct/src/gui/DirectCheckBox.py @@ -28,6 +28,7 @@ class DirectCheckBox(DirectButton): # Sounds to be used for button events ('rolloverSound', DGG.getDefaultRolloverSound(), self.setRolloverSound), ('clickSound', DGG.getDefaultClickSound(), self.setClickSound), + ('releaseSound', DGG.getDefaultReleaseSound(), self.setReleaseSound), # Can only be specified at time of widget contruction # Do the text/graphics appear to move when the button is clicked ('pressEffect', 1, DGG.INITOPT), diff --git a/direct/src/gui/DirectGuiGlobals.py b/direct/src/gui/DirectGuiGlobals.py index d41199af0e..1048f8d724 100644 --- a/direct/src/gui/DirectGuiGlobals.py +++ b/direct/src/gui/DirectGuiGlobals.py @@ -17,8 +17,9 @@ from panda3d.core import ( defaultFont = None defaultFontFunc = TextNode.getDefaultFont -defaultClickSound = None defaultRolloverSound = None +defaultClickSound = None +defaultReleaseSound = None defaultDialogGeom = None defaultDialogRelief = PGFrameStyle.TBevelOut drawOrder = 100 @@ -129,6 +130,13 @@ def setDefaultClickSound(newSound): global defaultClickSound defaultClickSound = newSound +def getDefaultReleaseSound(): + return defaultReleaseSound + +def setDefaultReleaseSound(newSound): + global defaultReleaseSound + defaultReleaseSound = newSound + def getDefaultFont(): global defaultFont if defaultFont is None: From 56cdea16c9f8a628810dcc6e90e0bd0aa4eb514e Mon Sep 17 00:00:00 2001 From: jtfedd Date: Sat, 15 Feb 2025 20:56:07 -0600 Subject: [PATCH 186/291] pgui: Fix bugs with pgSliderBar dragging Closes #1722 --- panda/src/pgui/pgSliderBar.cxx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/panda/src/pgui/pgSliderBar.cxx b/panda/src/pgui/pgSliderBar.cxx index d423549ccb..7fe5bc8c73 100644 --- a/panda/src/pgui/pgSliderBar.cxx +++ b/panda/src/pgui/pgSliderBar.cxx @@ -693,6 +693,11 @@ advance_scroll() { */ void PGSliderBar:: advance_page() { + // Do not try to advance the page while dragging + if (_dragging) { + return; + } + // Is the mouse position left or right of the current thumb position? LPoint3 mouse = mouse_to_local(_mouse_pos) - _thumb_start; PN_stdfloat target_ratio = mouse.dot(_axis) / _range_x; @@ -705,7 +710,7 @@ advance_page() { t = min(_ratio + _page_ratio - _scroll_ratio, target_ratio); } internal_set_ratio(t); - if (t == target_ratio) { + if (_ratio == target_ratio) { // We made it; begin dragging from now on until the user releases the // mouse. begin_drag(); From fd5258fe094f523b2a5e71f42131740e1a16c0f3 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 12 Nov 2025 11:36:42 +0100 Subject: [PATCH 187/291] makepanda: Skip building TinyOsxGraphicsWindow when building 64-bit Also disable linking AGL on macOS SDK 26.0, which has removed it --- makepanda/makepanda.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 17699c3074..d4879b869f 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1249,7 +1249,8 @@ if (COMPILER=="GCC"): if GetTarget() == 'darwin': LibName("ALWAYS", "-framework AppKit") - LibName("AGL", "-framework AGL") + if SDK["OSXTARGET"] < (26, 0): + LibName("AGL", "-framework AGL") LibName("CARBON", "-framework Carbon") LibName("COCOA", "-framework Cocoa") # Fix for a bug in OSX Leopard: @@ -5616,9 +5617,10 @@ if not RUNTIME and not PkgSkip("TINYDISPLAY"): TargetAdd('p3tinydisplay_ztriangle_4.obj', opts=OPTS, input='ztriangle_4.cxx') TargetAdd('p3tinydisplay_ztriangle_table.obj', opts=OPTS, input='ztriangle_table.cxx') if GetTarget() == 'darwin': - TargetAdd('p3tinydisplay_tinyOsxGraphicsWindow.obj', opts=OPTS, input='tinyOsxGraphicsWindow.mm') - TargetAdd('libp3tinydisplay.dll', input='p3tinydisplay_tinyOsxGraphicsWindow.obj') - TargetAdd('libp3tinydisplay.dll', opts=['CARBON', 'AGL', 'COCOA']) + if frozenset(OSX_ARCHS) - frozenset(('x86_64', 'arm64')): + TargetAdd('p3tinydisplay_tinyOsxGraphicsWindow.obj', opts=OPTS, input='tinyOsxGraphicsWindow.mm') + TargetAdd('libp3tinydisplay.dll', input='p3tinydisplay_tinyOsxGraphicsWindow.obj') + TargetAdd('libp3tinydisplay.dll', opts=['CARBON', 'AGL', 'COCOA']) elif GetTarget() == 'windows': TargetAdd('libp3tinydisplay.dll', input='libp3windisplay.dll') TargetAdd('libp3tinydisplay.dll', opts=['WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM']) From 23782ec51102c1b45aa53d7055c70ff8e7fab9a8 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 12 Nov 2025 11:40:50 +0100 Subject: [PATCH 188/291] task: Also check for subclasses of coroutine Apparently Nuitka uses custom coroutine classes that inherit from Python's own coroutine class, causing PyCoro_CheckExact to be inadequate In the future we may want to check the ABC or check the existence of send/throw methods --- panda/src/event/asyncFuture_ext.cxx | 2 +- panda/src/event/pythonTask.cxx | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/panda/src/event/asyncFuture_ext.cxx b/panda/src/event/asyncFuture_ext.cxx index 4bc9f07378..f30ca366d5 100644 --- a/panda/src/event/asyncFuture_ext.cxx +++ b/panda/src/event/asyncFuture_ext.cxx @@ -396,7 +396,7 @@ gather(PyObject *args) { continue; } #if PY_VERSION_HEX >= 0x03050000 - } else if (PyCoro_CheckExact(item)) { + } else if (PyObject_TypeCheck(item, &PyCoro_Type)) { // We allow passing in a coroutine instead of a future. This causes it // to be scheduled as a task on the current task manager. PT(AsyncTask) task = new PythonTask(item); diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index f5af3f48de..fabdeb4a6c 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -53,15 +53,18 @@ PythonTask(PyObject *func_or_coro, const std::string &name) : _function = func_or_coro; Py_INCREF(_function); #if PY_VERSION_HEX >= 0x03050000 - } else if (PyCoro_CheckExact(func_or_coro)) { - // We also allow passing in a coroutine, because why not. + } else if (PyCoro_CheckExact(func_or_coro) || + PyGen_CheckExact(func_or_coro) || + PyType_IsSubtype(Py_TYPE(func_or_coro), &PyCoro_Type)) { + // We also allow passing in a coroutine. _generator = func_or_coro; Py_INCREF(_generator); -#endif +#else } else if (PyGen_CheckExact(func_or_coro)) { // Something emulating a coroutine. _generator = func_or_coro; Py_INCREF(_generator); +#endif } else { nassert_raise("Invalid function passed to PythonTask"); } @@ -528,7 +531,7 @@ do_python_task() { Py_DECREF(str); Py_DECREF(str2); } - if (PyCoro_CheckExact(result)) { + if (PyObject_TypeCheck(result, &PyCoro_Type)) { // If a coroutine, am_await is possible but senseless, since we can // just call send(None) on the coroutine itself. _generator = result; @@ -624,7 +627,7 @@ do_python_task() { } #if PY_VERSION_HEX >= 0x03050000 - } else if (result == Py_None && PyCoro_CheckExact(_generator)) { + } else if (result == Py_None && PyObject_TypeCheck(_generator, &PyCoro_Type)) { // Bare yield from a coroutine means to try again next frame. Py_DECREF(result); return DS_cont; From a01e039669a10c38ab38eecfab9613662fa8ace7 Mon Sep 17 00:00:00 2001 From: omn14 Date: Wed, 16 Apr 2025 13:52:24 +0200 Subject: [PATCH 189/291] x11: Add function for detecting auto repeat key events (#1735) Cherry-picked from master --- panda/src/x11display/config_x11display.cxx | 4 ++ panda/src/x11display/config_x11display.h | 1 + panda/src/x11display/x11GraphicsWindow.cxx | 64 +++++++++++++++++++++- panda/src/x11display/x11GraphicsWindow.h | 2 + 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/panda/src/x11display/config_x11display.cxx b/panda/src/x11display/config_x11display.cxx index d31c4c1c23..e63d394e10 100644 --- a/panda/src/x11display/config_x11display.cxx +++ b/panda/src/x11display/config_x11display.cxx @@ -84,6 +84,10 @@ ConfigVariableString x_wm_class PRC_DESC("Specify the value to use for the res_class field of the window's " "WM_CLASS property.")); +ConfigVariableBool x_detectable_auto_repeat +("x-detectable-auto-repeat", false, + PRC_DESC("Set this true to enable detectable auto-repeat for keyboard input.")); + /** * Initializes the library. This must be called at least once before any of * the functions or classes in this library can be used. Normally it will be diff --git a/panda/src/x11display/config_x11display.h b/panda/src/x11display/config_x11display.h index 157f09c5fc..2373d99185 100644 --- a/panda/src/x11display/config_x11display.h +++ b/panda/src/x11display/config_x11display.h @@ -36,5 +36,6 @@ extern ConfigVariableInt x_wheel_right_button; extern ConfigVariableInt x_cursor_size; extern ConfigVariableString x_wm_class_name; extern ConfigVariableString x_wm_class; +extern ConfigVariableBool x_detectable_auto_repeat; #endif diff --git a/panda/src/x11display/x11GraphicsWindow.cxx b/panda/src/x11display/x11GraphicsWindow.cxx index 531a100e59..f6008e2940 100644 --- a/panda/src/x11display/x11GraphicsWindow.cxx +++ b/panda/src/x11display/x11GraphicsWindow.cxx @@ -97,8 +97,7 @@ x11GraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe, int flags, GraphicsStateGuardian *gsg, GraphicsOutput *host) : - GraphicsWindow(engine, pipe, name, fb_prop, win_prop, flags, gsg, host) -{ + GraphicsWindow(engine, pipe, name, fb_prop, win_prop, flags, gsg, host) { x11GraphicsPipe *x11_pipe; DCAST_INTO_V(x11_pipe, _pipe); _display = x11_pipe->get_display(); @@ -123,6 +122,8 @@ x11GraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe, PT(GraphicsWindowInputDevice) device = GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard_mouse"); add_input_device(device); _input = device; + + enable_detectable_auto_repeat(); } /** @@ -2442,3 +2443,62 @@ cleanup: return ret; } + +/** + * + */ +int x11GraphicsWindow:: +xim_preedit_start(XIC ic, XPointer client_data, XPointer call_data) { + x11GraphicsWindow *window = (x11GraphicsWindow *)client_data; + return window->handle_preedit_start(); +} + +/** + * + */ +void x11GraphicsWindow:: +xim_preedit_draw(XIC ic, XPointer client_data, + XIMPreeditDrawCallbackStruct *call_data) { + x11GraphicsWindow *window = (x11GraphicsWindow *)client_data; + nassertv_always(call_data != nullptr); + window->handle_preedit_draw(*call_data); +} + +/** + * + */ +void x11GraphicsWindow:: +xim_preedit_caret(XIC ic, XPointer client_data, + XIMPreeditCaretCallbackStruct *call_data) { + x11GraphicsWindow *window = (x11GraphicsWindow *)client_data; + nassertv_always(call_data != nullptr); + window->handle_preedit_caret(*call_data); +} + +/** + * + */ +void x11GraphicsWindow:: +xim_preedit_done(XIC ic, XPointer client_data, XPointer call_data) { + x11GraphicsWindow *window = (x11GraphicsWindow *)client_data; + window->handle_preedit_done(); +} + +/** + * Enables detectable auto-repeat if supported by the X server. + */ +void x11GraphicsWindow:: +enable_detectable_auto_repeat() { + if (!x_detectable_auto_repeat) { + return; + } + + Bool supported; + if (XkbSetDetectableAutoRepeat(_display, True, &supported)) { + x11display_cat.info() << "Detectable auto-repeat enabled.\n"; + } else if (!supported) { + x11display_cat.warning() << "Detectable auto-repeat is not supported by the X server.\n"; + } else { + x11display_cat.error() << "Failed to set detectable auto-repeat.\n"; + } +} diff --git a/panda/src/x11display/x11GraphicsWindow.h b/panda/src/x11display/x11GraphicsWindow.h index ca44e4d426..2b0ee94e3e 100644 --- a/panda/src/x11display/x11GraphicsWindow.h +++ b/panda/src/x11display/x11GraphicsWindow.h @@ -46,6 +46,8 @@ public: INLINE X11_Window get_xwindow() const; + void enable_detectable_auto_repeat(); + protected: virtual void close_window(); virtual bool open_window(); From 2c9997ee6d1802c3581436b919d984cf635be32a Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 12 Nov 2025 11:47:48 +0100 Subject: [PATCH 190/291] x11: Remove methods added erroneously during bad merge --- panda/src/x11display/x11GraphicsWindow.cxx | 40 ---------------------- 1 file changed, 40 deletions(-) diff --git a/panda/src/x11display/x11GraphicsWindow.cxx b/panda/src/x11display/x11GraphicsWindow.cxx index f6008e2940..dd6f468ebd 100644 --- a/panda/src/x11display/x11GraphicsWindow.cxx +++ b/panda/src/x11display/x11GraphicsWindow.cxx @@ -2444,46 +2444,6 @@ cleanup: return ret; } -/** - * - */ -int x11GraphicsWindow:: -xim_preedit_start(XIC ic, XPointer client_data, XPointer call_data) { - x11GraphicsWindow *window = (x11GraphicsWindow *)client_data; - return window->handle_preedit_start(); -} - -/** - * - */ -void x11GraphicsWindow:: -xim_preedit_draw(XIC ic, XPointer client_data, - XIMPreeditDrawCallbackStruct *call_data) { - x11GraphicsWindow *window = (x11GraphicsWindow *)client_data; - nassertv_always(call_data != nullptr); - window->handle_preedit_draw(*call_data); -} - -/** - * - */ -void x11GraphicsWindow:: -xim_preedit_caret(XIC ic, XPointer client_data, - XIMPreeditCaretCallbackStruct *call_data) { - x11GraphicsWindow *window = (x11GraphicsWindow *)client_data; - nassertv_always(call_data != nullptr); - window->handle_preedit_caret(*call_data); -} - -/** - * - */ -void x11GraphicsWindow:: -xim_preedit_done(XIC ic, XPointer client_data, XPointer call_data) { - x11GraphicsWindow *window = (x11GraphicsWindow *)client_data; - window->handle_preedit_done(); -} - /** * Enables detectable auto-repeat if supported by the X server. */ From 227a9e3406ed650f7ff6378643d36e0e7c8e7645 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 12 Nov 2025 11:48:51 +0100 Subject: [PATCH 191/291] workflow: Update from deprecated macOS 13 runner to macOS 14 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2feb1aa06e..e8695de55a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: - os: [ubuntu-22.04, windows-2022, macOS-13] + os: [ubuntu-22.04, windows-2022, macOS-14] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 From f91e5df142df829c70a270acb869963a7460c149 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:37:40 +0000 Subject: [PATCH 192/291] build(deps): bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/mypy.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e56441a51..9b8d60c012 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 10 @@ -375,7 +375,7 @@ jobs: os: [ubuntu-22.04, windows-2022, macOS-15] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install dependencies (Ubuntu) if: matrix.os == 'ubuntu-22.04' run: | @@ -498,7 +498,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install dependencies run: | @@ -587,7 +587,7 @@ jobs: runs-on: ${{ matrix.runner }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Android SDK uses: android-actions/setup-android@v3 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 5b2869902c..83a9d8138f 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -10,7 +10,7 @@ jobs: python-version: ['3.9', '3.13'] fail-fast: false steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: From 7ef2f0616f1838249c291ba9736d3a0aa4ee2751 Mon Sep 17 00:00:00 2001 From: Drew Rogers Date: Wed, 26 Nov 2025 06:27:37 -0600 Subject: [PATCH 193/291] Add a pandatool for converting various image formats to the TXO format. (#1740) --- makepanda/makepanda.py | 13 ++ pandatool/src/converter/CMakeLists.txt | 4 + pandatool/src/converter/txoConverter.cxx | 148 +++++++++++++++++++++++ pandatool/src/converter/txoConverter.h | 43 +++++++ 4 files changed, 208 insertions(+) create mode 100644 pandatool/src/converter/txoConverter.cxx create mode 100644 pandatool/src/converter/txoConverter.h diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index ad0b72777c..096388acd4 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -5284,6 +5284,19 @@ if not PkgSkip("PANDATOOL"): TargetAdd('egg2bam.exe', input=COMMON_EGG2X_LIBS) TargetAdd('egg2bam.exe', opts=['ADVAPI', 'FFTW']) +# +# DIRECTORY: pandatool/src/converter/ +# + +if not PkgSkip("PANDATOOL"): + OPTS=['DIR:pandatool/src/converter'] + TargetAdd('txo-converter_txoConverter.obj', opts=OPTS, input='txoConverter.cxx') + TargetAdd('txo-converter.exe', input='txo-converter_txoConverter.obj') + TargetAdd('txo-converter.exe', input='libp3progbase.lib') + TargetAdd('txo-converter.exe', input='libp3pandatoolbase.lib') + TargetAdd('txo-converter.exe', input=COMMON_PANDA_LIBS) + TargetAdd('txo-converter.exe', opts=['ADVAPI', 'FFTW']) + # # DIRECTORY: pandatool/src/daeegg/ # diff --git a/pandatool/src/converter/CMakeLists.txt b/pandatool/src/converter/CMakeLists.txt index a793b718ac..eccae3a774 100644 --- a/pandatool/src/converter/CMakeLists.txt +++ b/pandatool/src/converter/CMakeLists.txt @@ -21,3 +21,7 @@ install(TARGETS p3converter INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d ARCHIVE COMPONENT ToolsDevel) install(FILES ${P3CONVERTER_HEADERS} COMPONENT ToolsDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d) + +add_executable(txo-converter txoConverter.cxx txoConverter.h) +target_link_libraries(txo-converter p3progbase panda) +install(TARGETS txo-converter EXPORT Tools COMPONENT Tools DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/pandatool/src/converter/txoConverter.cxx b/pandatool/src/converter/txoConverter.cxx new file mode 100644 index 0000000000..a85a504d77 --- /dev/null +++ b/pandatool/src/converter/txoConverter.cxx @@ -0,0 +1,148 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file txoConverter.cxx + * @author RegDogg + * @date 2025-11-10 + */ + +#include "txoConverter.h" +#include "pnmFileTypeRegistry.h" +#include "pnmFileType.h" + +/** + * + */ +TxoConverter:: +TxoConverter() : WithOutputFile(true, false, true) { + set_program_brief("convert various image formats to .txo file format"); + set_program_description + ("This program reads the image date from the input file and " + "outputs a txo files, suitable for viewing in Panda."); + + clear_runlines(); + add_runline("[opts] input output"); + add_runline("[opts] -o output input"); + + add_option + ("o", "filename", 0, + "Specify the filename to which the resulting .bam file will be written. " + "If this option is omitted, the last parameter name is taken to be the " + "name of the output file.", + &TxoConverter::dispatch_filename, &_got_output_filename, &_output_filename); + + add_option + ("alpha", "filename", 0, + "Apply an RGB alpha file for image types such as JPEG.", + &TxoConverter::dispatch_filename, &_got_rgb_filename, &_rgb_filename); +} + +/** + * + */ +void TxoConverter:: +run() { + nassertv(has_output_filename()); + Filename fullpath = Filename(_image_filename.get_fullpath()); + + nout << "Reading " << fullpath << "...\n"; + + PNMFileType *type = PNMFileTypeRegistry::get_global_ptr()->get_type_from_extension(fullpath); + if (type == nullptr) { + nout << "Cannot determine type of image file " << fullpath << ".\n"; + nout << "Currently supported image types:\n"; + PNMFileTypeRegistry::get_global_ptr()->write(nout, 2); + nout << "\n"; + return; + } + + PT(Texture) tex = new Texture("original image"); + + if (_got_rgb_filename) { + PNMFileType *type = PNMFileTypeRegistry::get_global_ptr()->get_type_from_extension(_rgb_filename); + if (type == nullptr) { + nout << "Image file type '" << _rgb_filename << "' is unknown.\n"; + return; + } + tex->read(_image_filename, _rgb_filename.get_fullpath(), 0, 0, LoaderOptions()); + } + else { + tex->read(_image_filename, LoaderOptions()); + } + tex->get_ram_image(); + + convert_txo(tex); + +} + + +/** + * If the indicated Texture was not already loaded from a txo file, writes it + * to a txo file and updates the Texture object to reference the new file. + */ +void TxoConverter:: +convert_txo(Texture *tex) { + if (!tex->get_loaded_from_txo()) { + + Filename output = get_output_filename(); + output.make_dir(); + + if (tex->write(output)) { + nout << "Writing " << output << "...\n"; + tex->set_loaded_from_txo(); + tex->set_fullpath(output); + tex->clear_alpha_fullpath(); + + tex->set_filename(output); + tex->clear_alpha_filename(); + } + } +} + +/** + * + */ +bool TxoConverter:: +handle_args(ProgramBase::Args &args) { + if (!_got_output_filename && args.size() > 1) { + _got_output_filename = true; + _output_filename = Filename::from_os_specific(args.back()); + args.pop_back(); + } + + if (!_got_output_filename) { + nout << "You must specify an output path."; + return false; + } + + if ((_output_filename.get_extension() != "txo")) { + nout << "Output filename " << _output_filename << " must end in .txo\n"; + return false; + } + + if (args.empty()) { + nout << "You must specify the image file to read on the command line.\n"; + return false; + } + + if (args.size() != 1) { + nout << "Specify only one image on the command line.\n"; + return false; + } + + _image_filename = Filename::from_os_specific(args[0]); + + return true; +} + +int main(int argc, char *argv[]) { + TxoConverter prog; + prog.parse_command_line(argc, argv); + prog.run(); + return 0; +} \ No newline at end of file diff --git a/pandatool/src/converter/txoConverter.h b/pandatool/src/converter/txoConverter.h new file mode 100644 index 0000000000..9291e2e5b2 --- /dev/null +++ b/pandatool/src/converter/txoConverter.h @@ -0,0 +1,43 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file txoConverter.h + * @author RegDogg + * @date 2025-11-10 + */ + +#ifndef TXOCONVERTER_H +#define TXOCONVERTER_H + +#include "pandatoolbase.h" +#include "programBase.h" +#include "filename.h" +#include "withOutputFile.h" +#include "textureAttrib.h" + +/** + * + */ +class TxoConverter : public ProgramBase, public WithOutputFile { +public: + TxoConverter(); + + void run(); + + protected: + virtual bool handle_args(Args &args); + + private: + void convert_txo(Texture *tex); + + Filename _image_filename; + bool _got_rgb_filename; + Filename _rgb_filename; +}; + +#endif From a32d74314bc0b99d9303cf72235089d81e4d7137 Mon Sep 17 00:00:00 2001 From: George Aleksandrovich Date: Wed, 26 Nov 2025 04:34:07 -0800 Subject: [PATCH 194/291] Changed unsigned int to signed int for resync in DistributedSmoothNode. (#1162) Co-authored-by: rushingseas8 --- direct/src/distributed/direct.dc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/direct/src/distributed/direct.dc b/direct/src/distributed/direct.dc index 6468d91b29..b9387b20dc 100644 --- a/direct/src/distributed/direct.dc +++ b/direct/src/distributed/direct.dc @@ -84,8 +84,8 @@ dclass DistributedSmoothNode: DistributedNode { suggestResync(uint32 avId, int16 timestampA, int16 timestampB, int32 serverTimeSec, uint16 serverTimeUSec, - uint16 / 100 uncertainty); + int16 / 100 uncertainty); returnResync(uint32 avId, int16 timestampB, int32 serverTimeSec, uint16 serverTimeUSec, - uint16 / 100 uncertainty); + int16 / 100 uncertainty); }; From 1cfa61b435115d9010c987e794a86e09ea7bf63e Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 26 Nov 2025 11:16:51 +0100 Subject: [PATCH 195/291] showbase: Temporarily add back throw_new_frame This helps with slight version mismatch compat, we can remove it before release --- direct/src/showbase/showBase.cxx | 8 ++++++++ direct/src/showbase/showBase.h | 3 +++ 2 files changed, 11 insertions(+) diff --git a/direct/src/showbase/showBase.cxx b/direct/src/showbase/showBase.cxx index e7ccadaf2d..43a8a16f8e 100644 --- a/direct/src/showbase/showBase.cxx +++ b/direct/src/showbase/showBase.cxx @@ -21,6 +21,7 @@ extern "C" { void CPSEnableForegroundOperation(ProcessSerialNumber* psn); } #include "showBase.h" +#include "throw_event.h" #include "graphicsWindow.h" #include "renderBuffer.h" #include "camera.h" @@ -44,6 +45,13 @@ ConfigureDef(config_showbase); ConfigureFn(config_showbase) { } +// Throw the "NewFrame" event in the C++ world. Some of the lerp code depends +// on receiving this. +void +throw_new_frame() { + throw_event("NewFrame"); +} + // Initialize the application for making a Gui-based app, such as wx. At the // moment, this is a no-op except on Mac. void diff --git a/direct/src/showbase/showBase.h b/direct/src/showbase/showBase.h index bfea13462a..25cf5fdf14 100644 --- a/direct/src/showbase/showBase.h +++ b/direct/src/showbase/showBase.h @@ -16,6 +16,7 @@ #include "directbase.h" +#include "eventHandler.h" #include "graphicsWindow.h" #include "graphicsPipe.h" #include "animControl.h" @@ -31,6 +32,8 @@ class GraphicsEngine; BEGIN_PUBLISH +EXPCL_DIRECT_SHOWBASE void throw_new_frame(); + EXPCL_DIRECT_SHOWBASE void init_app_for_gui(); // to handle windows stickykeys From c32d5146b330c934d6c80024e91e0b8658e4fd72 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 26 Nov 2025 11:17:58 +0100 Subject: [PATCH 196/291] dist: Gracefully skip accidentally included Python .egg archives --- direct/src/dist/commands.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index d0b4af8814..cf0316b1ba 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -79,6 +79,10 @@ def _model_to_bam(_build_cmd, srcpath, dstpath): _register_python_loaders() + if src_fn.get_extension() == 'egg' and zipfile.is_zipfile(src_fn.to_os_specific()): + _build_cmd.warn('Skipping %s as it appears to be a Python .egg archive, was this included by mistake?' % (src_fn.to_os_specific())) + return + loader = p3d.Loader.get_global_ptr() options = p3d.LoaderOptions(p3d.LoaderOptions.LF_report_errors | p3d.LoaderOptions.LF_no_ram_cache) From 2ecbd546404202383453fad99c8e31474b6abf6c Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 1 Dec 2025 10:05:44 +0100 Subject: [PATCH 197/291] installer: Add Python 3.15 --- makepanda/installer.nsi | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/makepanda/installer.nsi b/makepanda/installer.nsi index b31ad3cd55..1a5f57a2bb 100644 --- a/makepanda/installer.nsi +++ b/makepanda/installer.nsi @@ -428,6 +428,7 @@ SectionGroup "Python modules" SecGroupPython !insertmacro PyBindingSection 3.12-32 .cp312-win32.pyd !insertmacro PyBindingSection 3.13-32 .cp313-win32.pyd !insertmacro PyBindingSection 3.14-32 .cp314-win32.pyd + !insertmacro PyBindingSection 3.15-32 .cp315-win32.pyd !else !insertmacro PyBindingSection 3.5 .cp35-win_amd64.pyd !insertmacro PyBindingSection 3.6 .cp36-win_amd64.pyd @@ -439,6 +440,7 @@ SectionGroup "Python modules" SecGroupPython !insertmacro PyBindingSection 3.12 .cp312-win_amd64.pyd !insertmacro PyBindingSection 3.13 .cp313-win_amd64.pyd !insertmacro PyBindingSection 3.14 .cp314-win_amd64.pyd + !insertmacro PyBindingSection 3.15 .cp315-win_amd64.pyd !endif SectionGroupEnd @@ -552,6 +554,7 @@ Function .onInit !insertmacro MaybeEnablePyBindingSection 3.12-32 !insertmacro MaybeEnablePyBindingSection 3.13-32 !insertmacro MaybeEnablePyBindingSection 3.14-32 + !insertmacro MaybeEnablePyBindingSection 3.15-32 ${EndIf} !else !insertmacro MaybeEnablePyBindingSection 3.5 @@ -565,6 +568,7 @@ Function .onInit !insertmacro MaybeEnablePyBindingSection 3.12 !insertmacro MaybeEnablePyBindingSection 3.13 !insertmacro MaybeEnablePyBindingSection 3.14 + !insertmacro MaybeEnablePyBindingSection 3.15 ${EndIf} !endif @@ -594,6 +598,10 @@ Function .onInit SectionSetFlags ${SecPyBindings3.14} ${SF_RO} SectionSetInstTypes ${SecPyBindings3.14} 0 !endif + !ifdef SecPyBindings3.15 + SectionSetFlags ${SecPyBindings3.15} ${SF_RO} + SectionSetInstTypes ${SecPyBindings3.15} 0 + !endif ${EndUnless} FunctionEnd @@ -909,6 +917,7 @@ Section Uninstall !insertmacro RemovePythonPath 3.12-32 !insertmacro RemovePythonPath 3.13-32 !insertmacro RemovePythonPath 3.14-32 + !insertmacro RemovePythonPath 3.15-32 !else !insertmacro RemovePythonPath 3.5 !insertmacro RemovePythonPath 3.6 @@ -920,6 +929,7 @@ Section Uninstall !insertmacro RemovePythonPath 3.12 !insertmacro RemovePythonPath 3.13 !insertmacro RemovePythonPath 3.14 + !insertmacro RemovePythonPath 3.15 !endif SetDetailsPrint both @@ -999,6 +1009,7 @@ SectionEnd !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.12-32} $(DESC_SecPyBindings3.12-32) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.13-32} $(DESC_SecPyBindings3.13-32) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.14-32} $(DESC_SecPyBindings3.14-32) + !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.15-32} $(DESC_SecPyBindings3.15-32) !else !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.5} $(DESC_SecPyBindings3.5) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.6} $(DESC_SecPyBindings3.6) @@ -1010,6 +1021,7 @@ SectionEnd !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.12} $(DESC_SecPyBindings3.12) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.13} $(DESC_SecPyBindings3.13) !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.14} $(DESC_SecPyBindings3.14) + !insertmacro MUI_DESCRIPTION_TEXT ${SecPyBindings3.15} $(DESC_SecPyBindings3.15) !endif !ifdef INCLUDE_PYVER !insertmacro MUI_DESCRIPTION_TEXT ${SecPython} $(DESC_SecPython) From 8ece2c86d018fbade96a61d50038a8d6ebb0bf73 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 1 Dec 2025 10:21:15 +0100 Subject: [PATCH 198/291] Update BACKERS.md from latest master [skip ci] --- BACKERS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BACKERS.md b/BACKERS.md index 2b48940fc3..29838e55e9 100644 --- a/BACKERS.md +++ b/BACKERS.md @@ -5,10 +5,12 @@ This is a list of all the people who are contributing financially to Panda3D. I ## Bronze Sponsors [Route4Me](https://route4me.com/) +[LambdaTest](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor) * [Daniel Stokes](https://opencollective.com/daniel-stokes) * [David Rose](https://opencollective.com/david-rose) * [Route4Me](https://route4me.com/) +* [LambdaTest](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor) ## Benefactors From 03873c7adfdf7cfc7c9ce94de2e6eaf54d24e0c9 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 1 Dec 2025 11:26:51 +0100 Subject: [PATCH 199/291] Update Windows thirdparty tools to 1.10.16 --- .github/workflows/ci.yml | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8695de55a..9c304c75af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,9 +20,9 @@ jobs: shell: powershell run: | $wc = New-Object System.Net.WebClient - $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.15/panda3d-1.10.15-tools-win64.zip", "thirdparty-tools.zip") + $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.16/panda3d-1.10.16-tools-win64.zip", "thirdparty-tools.zip") Expand-Archive -Path thirdparty-tools.zip - Move-Item -Path thirdparty-tools/panda3d-1.10.15/thirdparty -Destination . + Move-Item -Path thirdparty-tools/panda3d-1.10.16/thirdparty -Destination . - name: Get thirdparty packages (macOS) if: runner.os == 'macOS' run: | diff --git a/README.md b/README.md index 99ae98d97f..0621692a86 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ depending on whether you are on a 32-bit or 64-bit system, or you can [click here](https://github.com/rdb/panda3d-thirdparty) for instructions on building them from source. -- https://www.panda3d.org/download/panda3d-1.10.15/panda3d-1.10.15-tools-win64.zip -- https://www.panda3d.org/download/panda3d-1.10.15/panda3d-1.10.15-tools-win32.zip +- https://www.panda3d.org/download/panda3d-1.10.16/panda3d-1.10.16-tools-win64.zip +- https://www.panda3d.org/download/panda3d-1.10.16/panda3d-1.10.16-tools-win32.zip After acquiring these dependencies, you can build Panda3D from the command prompt using the following command. Change the `--msvc-version` option based From 6a77fc633b2187066820904cd694c42c90bb1cd2 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 1 Dec 2025 11:47:01 +0100 Subject: [PATCH 200/291] dist: Update macOS target for Python 3.14 to 10.15 --- direct/src/dist/commands.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index 68ce14ac82..b8b08b6456 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -301,7 +301,10 @@ class build_apps(setuptools.Command): # manylinux1 is not offered for Python 3.10 anymore self.platforms[0] = 'manylinux2010_x86_64' - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 14): + # This version of Python is only available for 10.15+. + self.platforms[1] = 'macosx_10_15_x86_64' + elif sys.version_info >= (3, 13): # This version of Python is only available for 10.13+. self.platforms[1] = 'macosx_10_13_x86_64' elif sys.version_info >= (3, 8): @@ -587,8 +590,14 @@ class build_apps(setuptools.Command): subprocess.check_call([sys.executable, '-m', 'pip'] + pip_args) except: # Display a more helpful message for these common issues. - if platform.startswith('macosx_10_9_') and sys.version_info >= (3, 13): - new_platform = platform.replace('macosx_10_9_', 'macosx_10_13_') + if platform.startswith('macosx_10_13_') and sys.version_info >= (3, 14): + new_platform = platform.replace('macosx_10_13_', 'macosx_10_15_') + self.announce('This error likely occurs because {} is not a supported target as of Python 3.14.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR) + elif platform.startswith('macosx_10_9_') and sys.version_info >= (3, 13): + if sys.version_info >= (3, 14): + new_platform = platform.replace('macosx_10_9_', 'macosx_10_15_') + else: + new_platform = platform.replace('macosx_10_9_', 'macosx_10_13_') self.announce('This error likely occurs because {} is not a supported target as of Python 3.13.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR) elif platform.startswith('manylinux2010_') and sys.version_info >= (3, 11): new_platform = platform.replace('manylinux2010_', 'manylinux2014_') @@ -597,7 +606,9 @@ class build_apps(setuptools.Command): new_platform = platform.replace('manylinux1_', 'manylinux2014_') self.announce('This error likely occurs because {} is not a supported target as of Python 3.10.\nChange the target platform to {} instead.'.format(platform, new_platform), distutils.log.ERROR) elif platform.startswith('macosx_10_6_') and sys.version_info >= (3, 8): - if sys.version_info >= (3, 13): + if sys.version_info >= (3, 14): + new_platform = platform.replace('macosx_10_6_', 'macosx_10_15_') + elif sys.version_info >= (3, 13): new_platform = platform.replace('macosx_10_6_', 'macosx_10_13_') else: new_platform = platform.replace('macosx_10_6_', 'macosx_10_9_') From f4ee9503557dc356e45751a85721eb6cd6c8c4fb Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 1 Dec 2025 14:46:40 +0100 Subject: [PATCH 201/291] deploy-stub: Fix missing time.h include --- pandatool/src/deploy-stub/deploy-stub.c | 1 + 1 file changed, 1 insertion(+) diff --git a/pandatool/src/deploy-stub/deploy-stub.c b/pandatool/src/deploy-stub/deploy-stub.c index 7a36da6789..95d40ad25a 100644 --- a/pandatool/src/deploy-stub/deploy-stub.c +++ b/pandatool/src/deploy-stub/deploy-stub.c @@ -21,6 +21,7 @@ #include #include #include +#include #if PY_MAJOR_VERSION >= 3 # include From 8d25975b57acc86b627c8d4055d4fa84597a61fb Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 6 Dec 2025 21:04:41 +0100 Subject: [PATCH 202/291] dist: Add hidden import for jnius --- direct/src/dist/FreezeTool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index 34f9a1d09f..b3a868edc0 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -102,6 +102,7 @@ defaultHiddenImports = { 'scipy.stats._stats': ['scipy.special.cython_special'], 'setuptools.monkey': ['setuptools.msvc'], 'shapely._geometry_helpers': ['shapely._geos'], + 'jnius': ['jnius_config'], } From d6da1965aea4ec603a7042490ded2676499e449d Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 6 Dec 2025 21:04:58 +0100 Subject: [PATCH 203/291] dist: Add linecache stubs for newer versions of Python --- direct/src/dist/FreezeTool.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index b3a868edc0..c6c38d8602 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -140,12 +140,23 @@ def getline(filename, lineno, module_globals=None): return '' def clearcache(): - global cache - cache = {} + cache.clear() def getlines(filename, module_globals=None): return [] +def _getline_from_code(filename, lineno): + return '' + +def _make_key(code): + return (code.co_filename, code.co_qualname, code.co_firstlineno) + +def _getlines_from_code(code): + return [] + +def _source_unavailable(filename): + return True + def checkcache(filename=None): pass From 8fb23c559944e698d68ab777f52c0b11bf2e6e41 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 6 Dec 2025 21:05:40 +0100 Subject: [PATCH 204/291] dist: Fix inability to pick up nested hidden modules --- direct/src/dist/FreezeTool.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index c6c38d8602..a883ec5fa2 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -1249,8 +1249,12 @@ class Freezer: pass # Check if any new modules we found have "hidden" imports - for origName in list(self.mf.modules.keys()): + checkHiddenImports = set(self.mf.modules.keys()) + while checkHiddenImports: + origName = next(iter(checkHiddenImports)) + checkHiddenImports.discard(origName) hidden = self.hiddenImports.get(origName, []) + preModules = frozenset(self.mf.modules.keys()) for modname in hidden: if modname.endswith('.*'): mdefs = self._gatherSubmodules(modname, implicit = True) @@ -1264,6 +1268,9 @@ class Freezer: self.__loadModule(self.ModuleDef(modname, implicit = True)) except ImportError: pass + addedModules = set(self.mf.modules.keys()) + addedModules -= preModules + checkHiddenImports |= addedModules # Special case for sysconfig, which depends on a platform-specific # sysconfigdata module on POSIX systems. From ba6b66fbb61bed2f0e510d84d28807bb31d6f667 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 6 Dec 2025 21:08:23 +0100 Subject: [PATCH 205/291] android: Fix crash when setting window title on wrong thread --- makepanda/makepanda.py | 2 ++ panda/src/android/PandaActivity.java | 12 ++++++++++++ panda/src/android/config_android.cxx | 17 ++++++----------- panda/src/android/config_android.h | 4 +--- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 096388acd4..7101b473d7 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -4946,12 +4946,14 @@ if GetTarget() == 'android': TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java') TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java') TargetAdd('org/panda3d/android/PandaActivity$1.class', opts=OPTS+['DEPENDENCYONLY'], input='PandaActivity.java') + TargetAdd('org/panda3d/android/PandaActivity$2.class', opts=OPTS+['DEPENDENCYONLY'], input='PandaActivity.java') TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.java') TargetAdd('classes.dex', input='org/panda3d/android/NativeIStream.class') TargetAdd('classes.dex', input='org/panda3d/android/NativeOStream.class') TargetAdd('classes.dex', input='org/panda3d/android/PandaActivity.class') TargetAdd('classes.dex', input='org/panda3d/android/PandaActivity$1.class') + TargetAdd('classes.dex', input='org/panda3d/android/PandaActivity$2.class') TargetAdd('classes.dex', input='org/panda3d/android/PythonActivity.class') TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx') diff --git a/panda/src/android/PandaActivity.java b/panda/src/android/PandaActivity.java index e3d6836407..4c7e64b4f8 100644 --- a/panda/src/android/PandaActivity.java +++ b/panda/src/android/PandaActivity.java @@ -159,6 +159,18 @@ public class PandaActivity extends NativeActivity { return getCacheDir().toString(); } + /** + * Sets the window title. + */ + public void setWindowTitle(final CharSequence title) { + final PandaActivity activity = this; + runOnUiThread(new Runnable() { + public void run() { + activity.setTitle(title); + } + }); + } + /** * Shows a pop-up notification. */ diff --git a/panda/src/android/config_android.cxx b/panda/src/android/config_android.cxx index 88ddba3c50..53c22c7b73 100644 --- a/panda/src/android/config_android.cxx +++ b/panda/src/android/config_android.cxx @@ -27,11 +27,9 @@ jmethodID jni_PandaActivity_readBitmap; jmethodID jni_PandaActivity_createBitmap; jmethodID jni_PandaActivity_compressBitmap; jmethodID jni_PandaActivity_showToast; +jmethodID jni_PandaActivity_setWindowTitle; jmethodID jni_PandaActivity_findLibrary; -jclass jni_Activity; -jmethodID jni_Activity_setTitle; - jclass jni_BitmapFactory_Options; jfieldID jni_BitmapFactory_Options_outWidth; jfieldID jni_BitmapFactory_Options_outHeight; @@ -81,17 +79,15 @@ jint JNI_OnLoad(JavaVM *jvm, void *reserved) { jni_PandaActivity_compressBitmap = env->GetStaticMethodID(jni_PandaActivity, "compressBitmap", "(Landroid/graphics/Bitmap;IIJ)Z"); + jni_PandaActivity_setWindowTitle = env->GetMethodID(jni_PandaActivity, + "setWindowTitle", "(Ljava/lang/CharSequence;)V"); + jni_PandaActivity_showToast = env->GetMethodID(jni_PandaActivity, "showToast", "(Ljava/lang/String;I)V"); jni_PandaActivity_findLibrary = env->GetMethodID(jni_PandaActivity, "findLibrary", "(Ljava/lang/String;)Ljava/lang/String;"); - jni_Activity = env->FindClass("android/app/Activity"); - jni_Activity = (jclass) env->NewGlobalRef(jni_Activity); - jni_Activity_setTitle = env->GetMethodID(jni_Activity, - "setTitle", "(Ljava/lang/CharSequence;)V"); - jni_BitmapFactory_Options = env->FindClass("android/graphics/BitmapFactory$Options"); jni_BitmapFactory_Options = (jclass) env->NewGlobalRef(jni_BitmapFactory_Options); @@ -130,7 +126,6 @@ void JNI_OnUnload(JavaVM *jvm, void *reserved) { nassertv(env != nullptr); env->DeleteGlobalRef(jni_PandaActivity); - env->DeleteGlobalRef(jni_Activity); env->DeleteGlobalRef(jni_BitmapFactory_Options); // These will no longer work without JNI, so unregister them. @@ -174,14 +169,14 @@ Filename android_find_library(ANativeActivity *activity, const std::string &lib) * Sets the window title of the activity. */ void android_set_title(ANativeActivity *activity, const std::string &title) { - nassertv(jni_Activity_setTitle); + nassertv(jni_PandaActivity_setWindowTitle); Thread *thread = Thread::get_current_thread(); JNIEnv *env = thread->get_jni_env(); nassertv(env != nullptr); jstring jmsg = env->NewStringUTF(title.c_str()); - env->CallVoidMethod(activity->clazz, jni_Activity_setTitle, jmsg); + env->CallVoidMethod(activity->clazz, jni_PandaActivity_setWindowTitle, jmsg); env->DeleteLocalRef(jmsg); } diff --git a/panda/src/android/config_android.h b/panda/src/android/config_android.h index 286e517a39..94a417c104 100644 --- a/panda/src/android/config_android.h +++ b/panda/src/android/config_android.h @@ -33,11 +33,9 @@ extern jmethodID jni_PandaActivity_readBitmapHeader; extern jmethodID jni_PandaActivity_readBitmap; extern jmethodID jni_PandaActivity_createBitmap; extern jmethodID jni_PandaActivity_compressBitmap; +extern jmethodID jni_PandaActivity_setWindowTitle; extern jmethodID jni_PandaActivity_showToast; -extern jclass jni_Activity; -extern jmethodID jni_Activity_setTitle; - extern jclass jni_BitmapFactory_Options; extern jfieldID jni_BitmapFactory_Options_outWidth; extern jfieldID jni_BitmapFactory_Options_outHeight; From ea55924e59bd7fdc9caa2f6d2e7b54b9c87bec8f Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 6 Dec 2025 21:09:57 +0100 Subject: [PATCH 206/291] android: Fix assorted compatibility issues with plyer/pyjnius pyjnius needs to do env->FindClass in the app thread, which only works if the app thread was created by Java, so I changed android_native_app_glue to do that instead of via pthreads --- makepanda/makepanda.py | 2 + panda/src/android/PandaActivity.java | 11 ++++++ panda/src/android/PythonActivity.java | 41 +++++++++++++++++++++ panda/src/android/android_native_app_glue.c | 9 +++++ panda/src/android/android_native_app_glue.h | 2 +- panda/src/android/config_android.cxx | 7 ++++ panda/src/android/config_android.h | 5 +++ panda/src/android/jni_PandaActivity.cxx | 8 ++++ 8 files changed, 84 insertions(+), 1 deletion(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 7101b473d7..f3bdb167af 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -4948,6 +4948,7 @@ if GetTarget() == 'android': TargetAdd('org/panda3d/android/PandaActivity$1.class', opts=OPTS+['DEPENDENCYONLY'], input='PandaActivity.java') TargetAdd('org/panda3d/android/PandaActivity$2.class', opts=OPTS+['DEPENDENCYONLY'], input='PandaActivity.java') TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.java') + TargetAdd('org/panda3d/android/PythonActivity$ActivityResultListener.class', opts=OPTS+['DEPENDENCYONLY'], input='PythonActivity.java') TargetAdd('classes.dex', input='org/panda3d/android/NativeIStream.class') TargetAdd('classes.dex', input='org/panda3d/android/NativeOStream.class') @@ -4955,6 +4956,7 @@ if GetTarget() == 'android': TargetAdd('classes.dex', input='org/panda3d/android/PandaActivity$1.class') TargetAdd('classes.dex', input='org/panda3d/android/PandaActivity$2.class') TargetAdd('classes.dex', input='org/panda3d/android/PythonActivity.class') + TargetAdd('classes.dex', input='org/panda3d/android/PythonActivity$ActivityResultListener.class') TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx') TargetAdd('libp3android.dll', input='p3android_composite1.obj') diff --git a/panda/src/android/PandaActivity.java b/panda/src/android/PandaActivity.java index 4c7e64b4f8..6c59886be4 100644 --- a/panda/src/android/PandaActivity.java +++ b/panda/src/android/PandaActivity.java @@ -81,6 +81,16 @@ public class PandaActivity extends NativeActivity { return Thread.currentThread().getName(); } + /** + * Called by android_native_app_glue to spawn the application thread. + * Gets passed a function pointer and a data pointer to pass to it. + */ + protected void spawnAppThread(long ptr, long data) { + new Thread(() -> { + nativeThreadEntry(ptr, data); + }).start(); + } + /** * Maps the blob to memory and returns the pointer. */ @@ -193,4 +203,5 @@ public class PandaActivity extends NativeActivity { } private static native long nativeMmap(int fd, long off, long len); + private static native void nativeThreadEntry(long ptr, long data); } diff --git a/panda/src/android/PythonActivity.java b/panda/src/android/PythonActivity.java index 731ed4d54c..68b4720be8 100644 --- a/panda/src/android/PythonActivity.java +++ b/panda/src/android/PythonActivity.java @@ -15,6 +15,12 @@ package org.panda3d.android; import org.panda3d.android.PandaActivity; +import android.content.Intent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + /** * Extends PandaActivity with some things that are useful in a Python * application. @@ -26,4 +32,39 @@ public class PythonActivity extends PandaActivity { public PythonActivity() { mActivity = this; } + + // Helper code to further support plyer. + public interface ActivityResultListener { + void onActivityResult(int requestCode, int resultCode, Intent data); + } + + private List activityResultListeners = null; + + public void registerActivityResultListener(ActivityResultListener listener) { + if (this.activityResultListeners == null) { + this.activityResultListeners = Collections.synchronizedList(new ArrayList()); + } + this.activityResultListeners.add(listener); + } + + public void unregisterActivityResultListener(ActivityResultListener listener) { + if (this.activityResultListeners == null) { + return; + } + this.activityResultListeners.remove(listener); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent intent) { + if (this.activityResultListeners == null) { + return; + } + this.onResume(); + synchronized (this.activityResultListeners) { + Iterator iterator = this.activityResultListeners.iterator(); + while (iterator.hasNext()) { + iterator.next().onActivityResult(requestCode, resultCode, intent); + } + } + } } diff --git a/panda/src/android/android_native_app_glue.c b/panda/src/android/android_native_app_glue.c index 1e63c5ef59..38aff2df51 100644 --- a/panda/src/android/android_native_app_glue.c +++ b/panda/src/android/android_native_app_glue.c @@ -255,10 +255,19 @@ static struct android_app* android_app_create(ANativeActivity* activity, android_app->msgread = msgpipe[0]; android_app->msgwrite = msgpipe[1]; + // Spawn the app thread in Java so it gets a valid class loader. + JNIEnv *env = activity->env; + jclass activity_class = (*env)->GetObjectClass(env, activity->clazz); + jmethodID method = (*env)->GetMethodID(env, activity_class, "spawnAppThread", "(JJ)V"); + LOGE("got function pointer: %ld, data: %ld, env: %ld", (jlong)(void *)android_app_entry, (jlong)(void *)android_app, (jlong)(void *)env); + (*env)->CallVoidMethod(env, activity->clazz, method, (void *)android_app_entry, (void *)android_app); + + /* pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_create(&android_app->thread, &attr, android_app_entry, android_app); + */ // Wait for thread to start. pthread_mutex_lock(&android_app->mutex); diff --git a/panda/src/android/android_native_app_glue.h b/panda/src/android/android_native_app_glue.h index 89eafe3ff9..7e4149e229 100644 --- a/panda/src/android/android_native_app_glue.h +++ b/panda/src/android/android_native_app_glue.h @@ -168,7 +168,7 @@ struct android_app { int msgread; int msgwrite; - pthread_t thread; + //pthread_t thread; struct android_poll_source cmdPollSource; struct android_poll_source inputPollSource; diff --git a/panda/src/android/config_android.cxx b/panda/src/android/config_android.cxx index 53c22c7b73..75b2d42a8a 100644 --- a/panda/src/android/config_android.cxx +++ b/panda/src/android/config_android.cxx @@ -193,3 +193,10 @@ void android_show_toast(ANativeActivity *activity, const std::string &message, i env->CallVoidMethod(activity->clazz, jni_PandaActivity_showToast, jmsg, (jint)duration); env->DeleteLocalRef(jmsg); } + +/** + * Returns the JNIEnv pointer corresponding to the current thread. + */ +void *SDL_AndroidGetJNIEnv() { + return Thread::get_current_thread()->get_jni_env(); +} diff --git a/panda/src/android/config_android.h b/panda/src/android/config_android.h index 94a417c104..9ff8b2107f 100644 --- a/panda/src/android/config_android.h +++ b/panda/src/android/config_android.h @@ -44,4 +44,9 @@ EXPORT_CLASS Filename android_find_library(ANativeActivity *activity, const std: EXPORT_CLASS void android_set_title(ANativeActivity *activity, const std::string &title); EXPORT_CLASS void android_show_toast(ANativeActivity *activity, const std::string &message, int duration); +// Used to support pyjnius +extern "C" { + EXPORT_CLASS void *SDL_AndroidGetJNIEnv(void); +}; + #endif diff --git a/panda/src/android/jni_PandaActivity.cxx b/panda/src/android/jni_PandaActivity.cxx index af1ae1e140..4d51e98091 100644 --- a/panda/src/android/jni_PandaActivity.cxx +++ b/panda/src/android/jni_PandaActivity.cxx @@ -38,3 +38,11 @@ Java_org_panda3d_android_PandaActivity_nativeMmap(JNIEnv* env, jclass, jint fd, return (jlong)0; } } + +/** + * Calls the given function pointer, passing the given data pointer. + */ +EXPORT_JNI void +Java_org_panda3d_android_PandaActivity_nativeThreadEntry(JNIEnv* env, jobject self, jlong func, jlong data) { + ((void (*)(void *))(void *)func)((void *)data); +} From 12facf4a59cfe4386be7def30fd134ad84d1caab Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 6 Dec 2025 21:10:44 +0100 Subject: [PATCH 207/291] x11: Set x-detectable-auto-repeat to true by default See #1735 --- panda/src/x11display/config_x11display.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/x11display/config_x11display.cxx b/panda/src/x11display/config_x11display.cxx index cfdea4e70c..36b33a4fb1 100644 --- a/panda/src/x11display/config_x11display.cxx +++ b/panda/src/x11display/config_x11display.cxx @@ -113,7 +113,7 @@ ConfigVariableBool x_send_startup_notification "that it no longer needs to display a spinning mouse cursor.")); ConfigVariableBool x_detectable_auto_repeat -("x-detectable-auto-repeat", false, +("x-detectable-auto-repeat", true, PRC_DESC("Set this true to enable detectable auto-repeat for keyboard input.")); /** From 55e9d3db664a6f288863013492ee0b15c8f32944 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:13:42 +0000 Subject: [PATCH 208/291] build(deps): bump actions/cache from 4 to 5 Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b8d60c012..80db1477f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: - name: Cache dependencies (Windows) if: runner.os == 'Windows' - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: thirdparty key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.15-r1 @@ -144,7 +144,7 @@ jobs: - name: ccache (non-Windows) if: runner.os != 'Windows' - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ccache key: ci-cmake-ccache-${{ matrix.profile }} @@ -513,7 +513,7 @@ jobs: - name: Restore Python build cache id: cache-emscripten-python-restore - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/python key: cache-emscripten-python-3.12.8 @@ -533,7 +533,7 @@ jobs: - name: Save Python build cache id: cache-emscripten-python-save if: steps.cache-emscripten-python-restore.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: ~/python key: ${{ steps.cache-emscripten-python-restore.outputs.cache-primary-key }} @@ -604,7 +604,7 @@ jobs: - name: Restore Python build cache id: cache-android-python-restore - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/python-prefix key: cache-android-python-3.13.9-${{ matrix.android-abi }}-ndk${{ matrix.ndk-version }} @@ -623,14 +623,14 @@ jobs: - name: Save Python build cache id: cache-android-python-save if: steps.cache-android-python-restore.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: ~/python-prefix key: ${{ steps.cache-android-python-restore.outputs.cache-primary-key }} - name: Restore thirdparty build cache id: cache-android-thirdparty-restore - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ./thirdparty key: cache-android-thirdparty-935c80380ca171e08587c570e9dad678d29db3c8-${{ matrix.android-abi }}-ndk${{ matrix.ndk-version }} @@ -662,7 +662,7 @@ jobs: - name: Save thirdparty build cache id: cache-android-thirdparty-save if: steps.cache-android-thirdparty-restore.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: ./thirdparty key: ${{ steps.cache-android-thirdparty-restore.outputs.cache-primary-key }} From 4705b329436bdb9717261eb97f3163d1155dae53 Mon Sep 17 00:00:00 2001 From: a2cy <75477334+a2cy@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:58:19 +0100 Subject: [PATCH 209/291] audiotraits: Add support for different coordinate systems for 3d audio --- panda/src/audiotraits/openalAudioManager.cxx | 176 ++++++++++++++++--- panda/src/audiotraits/openalAudioSound.cxx | 53 +++++- 2 files changed, 199 insertions(+), 30 deletions(-) diff --git a/panda/src/audiotraits/openalAudioManager.cxx b/panda/src/audiotraits/openalAudioManager.cxx index 50f4dcfcbc..d45060c33c 100644 --- a/panda/src/audiotraits/openalAudioManager.cxx +++ b/panda/src/audiotraits/openalAudioManager.cxx @@ -715,21 +715,85 @@ get_active() const { void OpenALAudioManager:: audio_3d_set_listener_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, PN_stdfloat vx, PN_stdfloat vy, PN_stdfloat vz, PN_stdfloat fx, PN_stdfloat fy, PN_stdfloat fz, PN_stdfloat ux, PN_stdfloat uy, PN_stdfloat uz) { ReMutexHolder holder(_lock); - _position[0] = px; - _position[1] = pz; - _position[2] = -py; + CoordinateSystem cs = get_default_coordinate_system(); - _velocity[0] = vx; - _velocity[1] = vz; - _velocity[2] = -vy; + switch (cs) { + case CS_yup_right: + _position[0] = px; + _position[1] = py; + _position[2] = pz; - _forward_up[0] = fx; - _forward_up[1] = fz; - _forward_up[2] = -fy; + _velocity[0] = vx; + _velocity[1] = vy; + _velocity[2] = vz; - _forward_up[3] = ux; - _forward_up[4] = uz; - _forward_up[5] = -uy; + _forward_up[0] = fx; + _forward_up[1] = fy; + _forward_up[2] = fz; + + _forward_up[3] = ux; + _forward_up[4] = uy; + _forward_up[5] = uz; + break; + + case CS_zup_right: + _position[0] = px; + _position[1] = pz; + _position[2] = -py; + + _velocity[0] = vx; + _velocity[1] = vz; + _velocity[2] = -vy; + + _forward_up[0] = fx; + _forward_up[1] = fz; + _forward_up[2] = -fy; + + _forward_up[3] = ux; + _forward_up[4] = uz; + _forward_up[5] = -uy; + break; + + case CS_yup_left: + _position[0] = px; + _position[1] = py; + _position[2] = -pz; + + _velocity[0] = vx; + _velocity[1] = vy; + _velocity[2] = -vz; + + _forward_up[0] = fx; + _forward_up[1] = fy; + _forward_up[2] = -fz; + + _forward_up[3] = ux; + _forward_up[4] = uy; + _forward_up[5] = -uz; + break; + + case CS_zup_left: + _position[0] = px; + _position[1] = pz; + _position[2] = py; + + _velocity[0] = vx; + _velocity[1] = vz; + _velocity[2] = vy; + + _forward_up[0] = fx; + _forward_up[1] = fz; + _forward_up[2] = fy; + + _forward_up[3] = ux; + _forward_up[4] = uz; + _forward_up[5] = uy; + break; + + default: + nassert_raise("invalid coordinate system"); + return; + } make_current(); @@ -749,21 +813,85 @@ audio_3d_set_listener_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, void OpenALAudioManager:: audio_3d_get_listener_attributes(PN_stdfloat *px, PN_stdfloat *py, PN_stdfloat *pz, PN_stdfloat *vx, PN_stdfloat *vy, PN_stdfloat *vz, PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz, PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz) { ReMutexHolder holder(_lock); - *px = _position[0]; - *py = -_position[2]; - *pz = _position[1]; + CoordinateSystem cs = get_default_coordinate_system(); - *vx = _velocity[0]; - *vy = -_velocity[2]; - *vz = _velocity[1]; + switch (cs) { + case CS_yup_right: + *px = _position[0]; + *py = _position[1]; + *pz = _position[2]; - *fx = _forward_up[0]; - *fy = -_forward_up[2]; - *fz = _forward_up[1]; + *vx = _velocity[0]; + *vy = _velocity[1]; + *vz = _velocity[2]; - *ux = _forward_up[3]; - *uy = -_forward_up[5]; - *uz = _forward_up[4]; + *fx = _forward_up[0]; + *fy = _forward_up[1]; + *fz = _forward_up[2]; + + *ux = _forward_up[3]; + *uy = _forward_up[4]; + *uz = _forward_up[5]; + break; + + case CS_zup_right: + *px = _position[0]; + *py = -_position[2]; + *pz = _position[1]; + + *vx = _velocity[0]; + *vy = -_velocity[2]; + *vz = _velocity[1]; + + *fx = _forward_up[0]; + *fy = -_forward_up[2]; + *fz = _forward_up[1]; + + *ux = _forward_up[3]; + *uy = -_forward_up[5]; + *uz = _forward_up[4]; + break; + + case CS_yup_left: + *px = _position[0]; + *py = _position[1]; + *pz = -_position[2]; + + *vx = _velocity[0]; + *vy = _velocity[1]; + *vz = -_velocity[2]; + + *fx = _forward_up[0]; + *fy = _forward_up[1]; + *fz = -_forward_up[2]; + + *ux = _forward_up[3]; + *uy = _forward_up[4]; + *uz = -_forward_up[5]; + break; + + case CS_zup_left: + *px = _position[0]; + *py = _position[2]; + *pz = _position[1]; + + *vx = _velocity[0]; + *vy = _velocity[2]; + *vz = _velocity[1]; + + *fx = _forward_up[0]; + *fy = _forward_up[2]; + *fz = _forward_up[1]; + + *ux = _forward_up[3]; + *uy = _forward_up[5]; + *uz = _forward_up[4]; + break; + + default: + nassert_raise("invalid coordinate system"); + return; + } } /** diff --git a/panda/src/audiotraits/openalAudioSound.cxx b/panda/src/audiotraits/openalAudioSound.cxx index f1bf7ef2ed..d334a4cd3d 100644 --- a/panda/src/audiotraits/openalAudioSound.cxx +++ b/panda/src/audiotraits/openalAudioSound.cxx @@ -14,6 +14,7 @@ // Panda Headers #include "throw_event.h" +#include "coordinateSystem.h" #include "openalAudioSound.h" #include "openalAudioManager.h" @@ -745,13 +746,53 @@ length() const { void OpenALAudioSound:: set_3d_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, PN_stdfloat vx, PN_stdfloat vy, PN_stdfloat vz) { ReMutexHolder holder(OpenALAudioManager::_lock); - _location[0] = px; - _location[1] = pz; - _location[2] = -py; + CoordinateSystem cs = get_default_coordinate_system(); - _velocity[0] = vx; - _velocity[1] = vz; - _velocity[2] = -vy; + switch (cs) { + case CS_yup_right: + _location[0] = px; + _location[1] = py; + _location[2] = pz; + + _velocity[0] = vx; + _velocity[1] = vy; + _velocity[2] = vz; + break; + + case CS_zup_right: + _location[0] = px; + _location[1] = pz; + _location[2] = -py; + + _velocity[0] = vx; + _velocity[1] = vz; + _velocity[2] = -vy; + break; + + case CS_yup_left: + _location[0] = px; + _location[1] = py; + _location[2] = -pz; + + _velocity[0] = vx; + _velocity[1] = vy; + _velocity[2] = -vz; + break; + + case CS_zup_left: + _location[0] = px; + _location[1] = pz; + _location[2] = py; + + _velocity[0] = vx; + _velocity[1] = vz; + _velocity[2] = vy; + break; + + default: + nassert_raise("invalid coordinate system"); + return; + } if (is_playing()) { _manager->make_current(); From 6acdcc5a8ad4911db859088980f06f9f1f6e1532 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 18 Dec 2025 13:41:38 +0100 Subject: [PATCH 210/291] patomic: Fix support for macOS pre-10.12 For now we will unconditionally use ulock APIs on arm64 (which requires macOS 11 anyway) and weak-link it (with fallback to pthreads implementation) on Intel See also #1780 --- dtool/src/dtoolbase/patomic.I | 12 ++++++++++++ dtool/src/dtoolbase/patomic.cxx | 2 +- dtool/src/dtoolbase/patomic.h | 13 +++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/dtool/src/dtoolbase/patomic.I b/dtool/src/dtoolbase/patomic.I index d1395c9e90..b679834713 100644 --- a/dtool/src/dtoolbase/patomic.I +++ b/dtool/src/dtoolbase/patomic.I @@ -454,6 +454,12 @@ patomic_notify_one(volatile uint32_t *value) { #elif defined(_WIN32) _patomic_wake_one_func((void *)value); #elif defined(__APPLE__) +#ifndef __arm64__ + if (UNLIKELY(__ulock_wake == nullptr || __ulock_wait == nullptr)) { + _patomic_notify_all(value); + return; + } +#endif __ulock_wake(UL_COMPARE_AND_WAIT, (void *)value, 0); #elif defined(HAVE_POSIX_THREADS) _patomic_notify_all(value); @@ -472,6 +478,12 @@ patomic_notify_all(volatile uint32_t *value) { #elif defined(_WIN32) _patomic_wake_all_func((void *)value); #elif defined(__APPLE__) +#ifndef __arm64__ + if (UNLIKELY(__ulock_wake == nullptr || __ulock_wait == nullptr)) { + _patomic_notify_all(value); + return; + } +#endif __ulock_wake(UL_COMPARE_AND_WAIT | ULF_WAKE_ALL, (void *)value, 0); #elif defined(HAVE_POSIX_THREADS) _patomic_notify_all(value); diff --git a/dtool/src/dtoolbase/patomic.cxx b/dtool/src/dtoolbase/patomic.cxx index 1b939a58c6..26949e564a 100644 --- a/dtool/src/dtoolbase/patomic.cxx +++ b/dtool/src/dtoolbase/patomic.cxx @@ -125,7 +125,7 @@ initialize_wait(volatile VOID *addr, PVOID cmp, SIZE_T size, DWORD timeout) { return emulated_wait(addr, cmp, size, timeout); } -#elif !defined(CPPPARSER) && !defined(__linux__) && !defined(__APPLE__) && defined(HAVE_POSIX_THREADS) +#elif !defined(CPPPARSER) && !defined(__linux__) && (!defined(__APPLE__) || !defined(__arm64__)) && defined(HAVE_POSIX_THREADS) // Same as above, but using pthreads. struct alignas(64) WaitTableEntry { diff --git a/dtool/src/dtoolbase/patomic.h b/dtool/src/dtoolbase/patomic.h index 02d1ab6ebc..2b0fdaacc0 100644 --- a/dtool/src/dtoolbase/patomic.h +++ b/dtool/src/dtoolbase/patomic.h @@ -36,9 +36,6 @@ // Undocumented API, see https://outerproduct.net/futex-dictionary.html #define UL_COMPARE_AND_WAIT 1 #define ULF_WAKE_ALL 0x00000100 - -extern "C" int __ulock_wait(uint32_t op, void *addr, uint64_t value, uint32_t timeout); -extern "C" int __ulock_wake(uint32_t op, void *addr, uint64_t wake_value); #endif #if defined(THREAD_DUMMY_IMPL) || defined(THREAD_SIMPLE_IMPL) @@ -173,9 +170,17 @@ ALWAYS_INLINE void patomic_notify_all(volatile uint32_t *value); EXPCL_DTOOL_DTOOLBASE extern BOOL (__stdcall *_patomic_wait_func)(volatile VOID *, PVOID, SIZE_T, DWORD); EXPCL_DTOOL_DTOOLBASE extern void (__stdcall *_patomic_wake_one_func)(PVOID); EXPCL_DTOOL_DTOOLBASE extern void (__stdcall *_patomic_wake_all_func)(PVOID); -#elif !defined(__linux__) && !defined(__APPLE__) && defined(HAVE_POSIX_THREADS) +#elif defined(__APPLE__) && defined(__arm64__) +extern "C" int __ulock_wait(uint32_t op, void *addr, uint64_t value, uint32_t timeout); +extern "C" int __ulock_wake(uint32_t op, void *addr, uint64_t wake_value); +#elif !defined(__linux__) && defined(HAVE_POSIX_THREADS) EXPCL_DTOOL_DTOOLBASE void _patomic_wait(const volatile uint32_t *value, uint32_t old); EXPCL_DTOOL_DTOOLBASE void _patomic_notify_all(volatile uint32_t *value); +#ifdef __APPLE__ +// Use conditionally since we can't count on support before 10.12. +extern "C" int __ulock_wait(uint32_t op, void *addr, uint64_t value, uint32_t timeout) __attribute__((weak_import)); +extern "C" int __ulock_wake(uint32_t op, void *addr, uint64_t wake_value) __attribute__((weak_import)); +#endif #endif #include "patomic.I" From 5f75553546888d25325c22f1a4129008f210584e Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 18 Dec 2025 13:45:06 +0100 Subject: [PATCH 211/291] makepanda: Remove reference to deprecated AGL framework on macOS --- makepanda/makepanda.py | 1 - 1 file changed, 1 deletion(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index f3bdb167af..6a0325e65b 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1071,7 +1071,6 @@ if (COMPILER=="GCC"): LibName("ALWAYS", "-framework AppKit") LibName("IOKIT", "-framework IOKit") LibName("QUARTZ", "-framework Quartz") - LibName("AGL", "-framework AGL") LibName("CARBON", "-framework Carbon") LibName("COCOA", "-framework Cocoa") # Fix for a bug in OSX Leopard: From f24bcf97392bdcf57303d4600c94c6f5ad68ac33 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 18 Dec 2025 13:47:00 +0100 Subject: [PATCH 212/291] ode: Add set_first_contact and set_backface_cull to OdeRayGeom Also deprecated get_params and set_params --- panda/src/ode/odeRayGeom.I | 10 ++++++++++ panda/src/ode/odeRayGeom.h | 6 ++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/panda/src/ode/odeRayGeom.I b/panda/src/ode/odeRayGeom.I index 94fdf2af15..235213be1c 100644 --- a/panda/src/ode/odeRayGeom.I +++ b/panda/src/ode/odeRayGeom.I @@ -63,6 +63,11 @@ get_params(int &first_contact, int &backface_cull) const { dGeomRayGetParams(_id, &first_contact, &backface_cull); } +INLINE void OdeRayGeom:: +set_first_contact(int first_contact) { + dGeomRaySetFirstContact(_id, first_contact); +} + INLINE int OdeRayGeom:: get_first_contact() const { int fc, bc; @@ -70,6 +75,11 @@ get_first_contact() const { return fc; } +INLINE void OdeRayGeom:: +set_backface_cull(int backface_cull) { + dGeomRaySetBackfaceCull(_id, backface_cull); +} + INLINE int OdeRayGeom:: get_backface_cull() const { int fc, bc; diff --git a/panda/src/ode/odeRayGeom.h b/panda/src/ode/odeRayGeom.h index 712f0e8440..9a9cfff166 100644 --- a/panda/src/ode/odeRayGeom.h +++ b/panda/src/ode/odeRayGeom.h @@ -41,9 +41,11 @@ PUBLISHED: INLINE void get(LVecBase3f &start, LVecBase3f &dir) const; INLINE LVecBase3f get_start() const; INLINE LVecBase3f get_direction() const; - INLINE void set_params(int first_contact, int backface_cull); - INLINE void get_params(int &first_contact, int &backface_cull) const; + [[deprecated]] INLINE void set_params(int first_contact, int backface_cull); + [[deprecated]] INLINE void get_params(int &first_contact, int &backface_cull) const; + INLINE void set_first_contact(int first_contact); INLINE int get_first_contact() const; + INLINE void set_backface_cull(int backface_cull); INLINE int get_backface_cull() const; INLINE void set_closest_hit(int closest_hit); INLINE int get_closest_hit(); From 784f32d0b989dc426ce7da1d72816688c2f565a2 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 18 Dec 2025 15:00:56 +0100 Subject: [PATCH 213/291] showbase: add user_exit alias for userExit --- direct/src/showbase/ShowBase.py | 1 + 1 file changed, 1 insertion(+) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 7db064fbea..ee1a415cb3 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -3396,6 +3396,7 @@ class ShowBase(DirectObject.DirectObject): remove_camera_frustum = removeCameraFrustum save_cube_map = saveCubeMap save_sphere_map = saveSphereMap + user_exit = userExit start_wx = startWx start_tk = startTk start_direct = startDirect From 6ada5071d531d0fb8d3942a65ef290f62089a1ff Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 6 Dec 2025 21:04:58 +0100 Subject: [PATCH 214/291] dist: Add linecache stubs for newer versions of Python --- direct/src/dist/FreezeTool.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index 93981a9c81..8f0d28d06b 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -201,12 +201,23 @@ def getline(filename, lineno, module_globals=None): return '' def clearcache(): - global cache - cache = {} + cache.clear() def getlines(filename, module_globals=None): return [] +def _getline_from_code(filename, lineno): + return '' + +def _make_key(code): + return (code.co_filename, code.co_qualname, code.co_firstlineno) + +def _getlines_from_code(code): + return [] + +def _source_unavailable(filename): + return True + def checkcache(filename=None): pass From 5b2cb98a9363023d0be9489e79d9ead390c24dbd Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 6 Dec 2025 21:04:41 +0100 Subject: [PATCH 215/291] dist: Add hidden import for jnius --- direct/src/dist/FreezeTool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/direct/src/dist/FreezeTool.py b/direct/src/dist/FreezeTool.py index 8f0d28d06b..eeed0fa35b 100644 --- a/direct/src/dist/FreezeTool.py +++ b/direct/src/dist/FreezeTool.py @@ -115,6 +115,7 @@ hiddenImports = { 'scipy.stats._stats': ['scipy.special.cython_special'], 'setuptools.monkey': ['setuptools.msvc'], 'shapely._geometry_helpers': ['shapely._geos'], + 'jnius': ['jnius_config'], } if sys.version_info >= (3,): From 2759fb9606ee743660dafac61ed60bba34f9c0f8 Mon Sep 17 00:00:00 2001 From: "Bernhard M. Wiedemann" Date: Thu, 18 Dec 2025 15:19:32 +0000 Subject: [PATCH 216/291] build: Allow to build for riscv64 --- dtool/src/dtoolbase/dtool_platform.h | 3 +++ makepanda/makepanda.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dtool/src/dtoolbase/dtool_platform.h b/dtool/src/dtoolbase/dtool_platform.h index dbfcb26556..dce9e989f3 100644 --- a/dtool/src/dtoolbase/dtool_platform.h +++ b/dtool/src/dtoolbase/dtool_platform.h @@ -96,6 +96,9 @@ #elif defined(__arm__) #define DTOOL_PLATFORM "linux_arm" +#elif defined(__riscv) +#define DTOOL_PLATFORM "linux_riscv" + #elif defined(__ppc__) #define DTOOL_PLATFORM "linux_ppc" diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 6a0325e65b..bb2b99becf 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1427,7 +1427,7 @@ def CompileCxx(obj,src,opts): if optlevel >= 4 or target == "android": cmd += " -fno-rtti" - if ('SSE2' in opts or not PkgSkip("SSE2")) and not arch.startswith("arm") and arch != 'aarch64': + if ('SSE2' in opts or not PkgSkip("SSE2")) and arch.find('86') > 0: if GetTarget() != "emscripten": cmd += " -msse2" From bd1105d545075f8f411e429161e7538b3b54c27f Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 10:06:45 +0100 Subject: [PATCH 217/291] directnotify: Add snake_case versions of methods --- direct/src/directnotify/DirectNotify.py | 37 +++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/direct/src/directnotify/DirectNotify.py b/direct/src/directnotify/DirectNotify.py index f7c7e6c964..8b93088362 100644 --- a/direct/src/directnotify/DirectNotify.py +++ b/direct/src/directnotify/DirectNotify.py @@ -42,25 +42,25 @@ class DirectNotify: """ return list(self.__categories.keys()) - def getCategory(self, categoryName: str) -> Notifier.Notifier | None: + def getCategory(self, name: str) -> Notifier.Notifier | None: """getCategory(self, string) Return the category with given name if present, None otherwise """ - return self.__categories.get(categoryName, None) + return self.__categories.get(name, None) - def newCategory(self, categoryName: str, logger: Logger.Logger | None = None) -> Notifier.Notifier: + def newCategory(self, name: str, logger: Logger.Logger | None = None) -> Notifier.Notifier: """newCategory(self, string) - Make a new notify category named categoryName. Return new category + Make a new notify category named name. Return new category if no such category exists, else return existing category """ - if categoryName not in self.__categories: - self.__categories[categoryName] = Notifier.Notifier(categoryName, logger) - self.setDconfigLevel(categoryName) - notifier = self.getCategory(categoryName) + if name not in self.__categories: + self.__categories[name] = Notifier.Notifier(name, logger) + self.setDconfigLevel(name) + notifier = self.getCategory(name) assert notifier is not None return notifier - def setDconfigLevel(self, categoryName: str) -> None: + def setDconfigLevel(self, name: str) -> None: """ Check to see if this category has a dconfig variable to set the notify severity and then set that level. You cannot @@ -71,7 +71,7 @@ class DirectNotify: # we're running before ShowBase has finished initializing from panda3d.core import ConfigVariableString - dconfigParam = ("notify-level-" + categoryName) + dconfigParam = ("notify-level-" + name) cvar = ConfigVariableString(dconfigParam, "") level = cvar.getValue() @@ -82,11 +82,11 @@ class DirectNotify: if not level: level = 'error' - category = self.getCategory(categoryName) - assert category is not None, f'failed to find category: {categoryName!r}' + category = self.getCategory(name) + assert category is not None, f'failed to find category: {name!r}' # Note - this print statement is making it difficult to # achieve "no output unless there's an error" operation - Josh - # print ("Setting DirectNotify category: " + categoryName + + # print ("Setting DirectNotify category: " + name + # " to severity: " + level) if level == "error": category.setWarning(False) @@ -106,7 +106,7 @@ class DirectNotify: category.setDebug(True) else: print("DirectNotify: unknown notify level: " + str(level) - + " for category: " + str(categoryName)) + + " for category: " + str(name)) def setDconfigLevels(self) -> None: for categoryName in self.getCategories(): @@ -129,3 +129,12 @@ class DirectNotify: def giveNotify(self, cls) -> None: cls.notify = self.newCategory(cls.__name__) + + get_categories = getCategories + get_category = getCategory + new_category = newCategory + set_dconfig_level = setDconfigLevel + set_dconfig_levels = setDconfigLevels + set_verbose = setVerbose + popup_controls = popupControls + give_notify = giveNotify From 5d8af37c41c92005cd24b4e18c67e50515d00946 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 11:37:16 +0100 Subject: [PATCH 218/291] makepanda: Only add -msse2 when building for x86 platforms Partial backport of 2759fb9606ee743660dafac61ed60bba34f9c0f8 --- makepanda/makepanda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index d4879b869f..2fd81b6624 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -1658,7 +1658,7 @@ def CompileCxx(obj,src,opts): if optlevel >= 4 or target == "android": cmd += " -fno-rtti" - if ('SSE2' in opts or not PkgSkip("SSE2")) and not arch.startswith("arm") and arch != 'aarch64': + if ('SSE2' in opts or not PkgSkip("SSE2")) and arch.find('86') > 0: cmd += " -msse2" # Needed by both Python, Panda, Eigen, all of which break aliasing rules. From 04e5cbe70646d1926408199b995077e4eed4d51c Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 11:04:35 +0100 Subject: [PATCH 219/291] Add support for (occlusion-)metallic-roughness textures Existing "selector" mode (which was unused and has been used to implement ORM textures has been renamed to metallic-roughness, and new modes have been added for occlusion and occlusion-metallic-roughness. New GLSL input names have been added. The .egg format has been extended. The Assimp loader has been updated. The shader generator now handles occlusion maps, and a unit test for that is added. --- panda/src/display/graphicsStateGuardian.cxx | 27 ++++++++++++- panda/src/doc/eggSyntax.txt | 5 ++- panda/src/egg/eggTexture.cxx | 27 +++++++++---- panda/src/egg/eggTexture.h | 5 ++- panda/src/egg2pg/eggLoader.cxx | 12 +++++- panda/src/egg2pg/eggSaver.cxx | 10 ++++- panda/src/glstuff/glShaderContext_src.cxx | 8 +++- panda/src/gobj/shader.h | 3 +- panda/src/gobj/textureStage.cxx | 10 ++++- panda/src/gobj/textureStage.h | 6 ++- panda/src/grutil/multitexReducer.cxx | 4 +- panda/src/pgraphnodes/shaderGenerator.cxx | 19 ++++++++- panda/src/pgraphnodes/shaderGenerator.h | 1 + pandatool/src/assimp/assimpLoader.cxx | 45 +++++++++++++++------ pandatool/src/assimp/assimpLoader.h | 6 +-- tests/display/test_color_buffer.py | 28 +++++++++++++ 16 files changed, 176 insertions(+), 40 deletions(-) diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index d94c7b5972..16518d79b4 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -2496,7 +2496,7 @@ fetch_specified_texture(Shader::ShaderTexSpec &spec, SamplerState &sampler, return default_normal_height_tex; } - case Shader::STO_stage_selector_i: + case Shader::STO_stage_metallic_roughness_i: { const TextureAttrib *texattrib; if (_target_rs->get_attrib(texattrib)) { @@ -2505,7 +2505,8 @@ fetch_specified_texture(Shader::ShaderTexSpec &spec, SamplerState &sampler, TextureStage *stage = texattrib->get_on_stage(i); TextureStage::Mode mode = stage->get_mode(); - if (mode == TextureStage::M_selector) { + if (mode == TextureStage::M_metallic_roughness || + mode == TextureStage::M_occlusion_metallic_roughness) { if (si++ == spec._stage) { sampler = texattrib->get_on_sampler(stage); view += stage->get_tex_view_offset(); @@ -2538,6 +2539,28 @@ fetch_specified_texture(Shader::ShaderTexSpec &spec, SamplerState &sampler, } break; + case Shader::STO_stage_occlusion_i: + { + const TextureAttrib *texattrib; + if (_target_rs->get_attrib(texattrib)) { + int si = 0; + for (int i = 0; i < texattrib->get_num_on_stages(); ++i) { + TextureStage *stage = texattrib->get_on_stage(i); + TextureStage::Mode mode = stage->get_mode(); + + if (mode == TextureStage::M_occlusion || + mode == TextureStage::M_occlusion_metallic_roughness) { + if (si++ == spec._stage) { + sampler = texattrib->get_on_sampler(stage); + view += stage->get_tex_view_offset(); + return texattrib->get_on_texture(stage); + } + } + } + } + } + break; + default: nassertr(false, nullptr); break; diff --git a/panda/src/doc/eggSyntax.txt b/panda/src/doc/eggSyntax.txt index b81eb45c0e..c8225a66df 100644 --- a/panda/src/doc/eggSyntax.txt +++ b/panda/src/doc/eggSyntax.txt @@ -339,7 +339,10 @@ appear before they are referenced. *GLOW *GLOSS *HEIGHT - *SELECTOR + *EMISSION + *METALLIC_ROUGHNESS + *OCCLUSION + *OCCLUSION_METALLIC_ROUGHNESS The default environment type is MODULATE, which means the texture color is multiplied with the base polygon (or vertex) color. This diff --git a/panda/src/egg/eggTexture.cxx b/panda/src/egg/eggTexture.cxx index 6c78d57b3d..8b4e261455 100644 --- a/panda/src/egg/eggTexture.cxx +++ b/panda/src/egg/eggTexture.cxx @@ -561,11 +561,11 @@ affects_polygon_alpha() const { case ET_height: case ET_normal_gloss: case ET_emission: + case ET_occlusion: + case ET_metallic_roughness: + case ET_occlusion_metallic_roughness: return false; - case ET_selector: - return true; - case ET_unspecified: break; } @@ -883,8 +883,9 @@ string_env_type(const string &string) { } else if (cmp_nocase_uh(string, "height") == 0) { return ET_height; - } else if (cmp_nocase_uh(string, "selector") == 0) { - return ET_selector; + } else if (cmp_nocase_uh(string, "metallic_roughness") == 0 || + cmp_nocase_uh(string, "selector") == 0) { + return ET_metallic_roughness; } else if (cmp_nocase_uh(string, "normal_gloss") == 0) { return ET_normal_gloss; @@ -892,6 +893,12 @@ string_env_type(const string &string) { } else if (cmp_nocase_uh(string, "emission") == 0) { return ET_emission; + } else if (cmp_nocase_uh(string, "occlusion") == 0) { + return ET_occlusion; + + } else if (cmp_nocase_uh(string, "occlusion_metallic_roughness") == 0) { + return ET_occlusion_metallic_roughness; + } else { return ET_unspecified; } @@ -1321,14 +1328,20 @@ ostream &operator << (ostream &out, EggTexture::EnvType type) { case EggTexture::ET_height: return out << "height"; - case EggTexture::ET_selector: - return out << "selector"; + case EggTexture::ET_metallic_roughness: + return out << "metallic_roughness"; case EggTexture::ET_normal_gloss: return out << "normal_gloss"; case EggTexture::ET_emission: return out << "emission"; + + case EggTexture::ET_occlusion: + return out << "occlusion"; + + case EggTexture::ET_occlusion_metallic_roughness: + return out << "occlusion_metallic_roughness"; } nassertr(false, out); diff --git a/panda/src/egg/eggTexture.h b/panda/src/egg/eggTexture.h index e67cc2cdb3..8a8435c9e9 100644 --- a/panda/src/egg/eggTexture.h +++ b/panda/src/egg/eggTexture.h @@ -106,9 +106,12 @@ PUBLISHED: ET_glow, ET_gloss, ET_height, - ET_selector, + ET_metallic_roughness, ET_normal_gloss, ET_emission, + ET_occlusion, + ET_occlusion_metallic_roughness, + ET_selector = ET_metallic_roughness, }; enum CombineMode { CM_unspecified, diff --git a/panda/src/egg2pg/eggLoader.cxx b/panda/src/egg2pg/eggLoader.cxx index e0dca70801..2b5a037cab 100644 --- a/panda/src/egg2pg/eggLoader.cxx +++ b/panda/src/egg2pg/eggLoader.cxx @@ -1579,8 +1579,8 @@ make_texture_stage(const EggTexture *egg_tex) { stage->set_mode(TextureStage::M_height); break; - case EggTexture::ET_selector: - stage->set_mode(TextureStage::M_selector); + case EggTexture::ET_metallic_roughness: + stage->set_mode(TextureStage::M_metallic_roughness); break; case EggTexture::ET_normal_gloss: @@ -1591,6 +1591,14 @@ make_texture_stage(const EggTexture *egg_tex) { stage->set_mode(TextureStage::M_emission); break; + case EggTexture::ET_occlusion: + stage->set_mode(TextureStage::M_occlusion); + break; + + case EggTexture::ET_occlusion_metallic_roughness: + stage->set_mode(TextureStage::M_occlusion_metallic_roughness); + break; + case EggTexture::ET_unspecified: break; } diff --git a/panda/src/egg2pg/eggSaver.cxx b/panda/src/egg2pg/eggSaver.cxx index ebfaf3d750..141a09549d 100644 --- a/panda/src/egg2pg/eggSaver.cxx +++ b/panda/src/egg2pg/eggSaver.cxx @@ -852,8 +852,8 @@ convert_primitive(const GeomVertexData *vertex_data, case TextureStage::M_height: egg_tex->set_env_type(EggTexture::ET_height); break; - case TextureStage::M_selector: - egg_tex->set_env_type(EggTexture::ET_selector); + case TextureStage::M_metallic_roughness: + egg_tex->set_env_type(EggTexture::ET_metallic_roughness); break; case TextureStage::M_normal_gloss: egg_tex->set_env_type(EggTexture::ET_normal_gloss); @@ -861,6 +861,12 @@ convert_primitive(const GeomVertexData *vertex_data, case TextureStage::M_emission: egg_tex->set_env_type(EggTexture::ET_emission); break; + case TextureStage::M_occlusion: + egg_tex->set_env_type(EggTexture::ET_occlusion); + break; + case TextureStage::M_occlusion_metallic_roughness: + egg_tex->set_env_type(EggTexture::ET_occlusion_metallic_roughness); + break; default: break; } diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx index 9826fad4bb..37140ce9f0 100644 --- a/panda/src/glstuff/glShaderContext_src.cxx +++ b/panda/src/glstuff/glShaderContext_src.cxx @@ -1043,8 +1043,9 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) { else if (noprefix.compare(7, string::npos, "Height") == 0) { bind._part = Shader::STO_stage_height_i; } - else if (noprefix.compare(7, string::npos, "Selector") == 0) { - bind._part = Shader::STO_stage_selector_i; + else if (noprefix.compare(7, string::npos, "MetallicRoughness") == 0 || + noprefix.compare(7, string::npos, "Selector") == 0) { + bind._part = Shader::STO_stage_metallic_roughness_i; } else if (noprefix.compare(7, string::npos, "Gloss") == 0) { bind._part = Shader::STO_stage_gloss_i; @@ -1052,6 +1053,9 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) { else if (noprefix.compare(7, string::npos, "Emission") == 0) { bind._part = Shader::STO_stage_emission_i; } + else if (noprefix.compare(7, string::npos, "Occlusion") == 0) { + bind._part = Shader::STO_stage_occlusion_i; + } else { GLCAT.error() << "Unrecognized shader input name: p3d_" << noprefix << "\n"; diff --git a/panda/src/gobj/shader.h b/panda/src/gobj/shader.h index 4bd548d331..88021d2403 100644 --- a/panda/src/gobj/shader.h +++ b/panda/src/gobj/shader.h @@ -232,9 +232,10 @@ public: STO_stage_add_i, STO_stage_normal_i, STO_stage_height_i, - STO_stage_selector_i, + STO_stage_metallic_roughness_i, STO_stage_gloss_i, STO_stage_emission_i, + STO_stage_occlusion_i, }; enum ShaderArgClass { diff --git a/panda/src/gobj/textureStage.cxx b/panda/src/gobj/textureStage.cxx index 3540deb92d..8292083773 100644 --- a/panda/src/gobj/textureStage.cxx +++ b/panda/src/gobj/textureStage.cxx @@ -519,14 +519,20 @@ operator << (ostream &out, TextureStage::Mode mode) { case TextureStage::M_height: return out << "height"; - case TextureStage::M_selector: - return out << "selector"; + case TextureStage::M_metallic_roughness: + return out << "metallic_roughness"; case TextureStage::M_normal_gloss: return out << "normal_gloss"; case TextureStage::M_emission: return out << "emission"; + + case TextureStage::M_occlusion: + return out << "occlusion"; + + case TextureStage::M_occlusion_metallic_roughness: + return out << "occlusion_metallic_roughness"; } return out << "**invalid Mode(" << (int)mode << ")**"; diff --git a/panda/src/gobj/textureStage.h b/panda/src/gobj/textureStage.h index 10a9bc5251..ff5bcb82be 100644 --- a/panda/src/gobj/textureStage.h +++ b/panda/src/gobj/textureStage.h @@ -61,10 +61,14 @@ PUBLISHED: M_glow, // Rarely used: modulate_glow is more efficient. M_gloss, // Rarely used: modulate_gloss is more efficient. M_height, // Rarely used: normal_height is more efficient. - M_selector, + M_metallic_roughness, // metalness in B, roughness in G M_normal_gloss, M_emission, + M_occlusion, // In red channel + M_occlusion_metallic_roughness, + + M_selector = M_metallic_roughness, }; enum CombineMode { diff --git a/panda/src/grutil/multitexReducer.cxx b/panda/src/grutil/multitexReducer.cxx index fd94e1d173..f2b3107051 100644 --- a/panda/src/grutil/multitexReducer.cxx +++ b/panda/src/grutil/multitexReducer.cxx @@ -712,9 +712,11 @@ make_texture_layer(const NodePath &render, case TextureStage::M_glow: case TextureStage::M_gloss: case TextureStage::M_height: - case TextureStage::M_selector: + case TextureStage::M_metallic_roughness: case TextureStage::M_normal_gloss: case TextureStage::M_emission: + case TextureStage::M_occlusion: + case TextureStage::M_occlusion_metallic_roughness: // Don't know what to do with these funny modes. We should probably raise // an exception or something. Fall through for now. diff --git a/panda/src/pgraphnodes/shaderGenerator.cxx b/panda/src/pgraphnodes/shaderGenerator.cxx index 5a8a17c9df..dd5e9f4c11 100644 --- a/panda/src/pgraphnodes/shaderGenerator.cxx +++ b/panda/src/pgraphnodes/shaderGenerator.cxx @@ -491,6 +491,10 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) { case TextureStage::M_emission: info._flags = ShaderKey::TF_map_emission; break; + case TextureStage::M_occlusion: + case TextureStage::M_occlusion_metallic_roughness: + info._flags = ShaderKey::TF_map_occlusion; + break; default: break; } @@ -557,9 +561,13 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) { // Decide whether to separate ambient and diffuse calculations. if (have_ambient) { - if (key._material_flags & Material::F_ambient) { + if (key._texture_flags & ShaderKey::TF_map_occlusion) { key._have_separate_ambient = true; - } else { + } + else if (key._material_flags & Material::F_ambient) { + key._have_separate_ambient = true; + } + else { if (key._material_flags & Material::F_diffuse) { key._have_separate_ambient = true; } else { @@ -1599,6 +1607,13 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) { text << "\t result *= saturate(2 * (tex" << map_index_glow << ".a - 0.5));\n"; } if (key._have_separate_ambient) { + // Apply occlusion textures. + for (size_t i = 0; i < key._textures.size(); ++i) { + ShaderKey::TextureInfo &tex = key._textures[i]; + if (tex._flags & ShaderKey::TF_map_occlusion) { + text << "\t tot_ambient *= tex" << i << ".r;\n"; + } + } if (key._material_flags & Material::F_ambient) { text << "\t result += tot_ambient * attr_material[0];\n"; } else if (key._color_type == ColorAttrib::T_vertex) { diff --git a/panda/src/pgraphnodes/shaderGenerator.h b/panda/src/pgraphnodes/shaderGenerator.h index f23fe3b315..fe574707f1 100644 --- a/panda/src/pgraphnodes/shaderGenerator.h +++ b/panda/src/pgraphnodes/shaderGenerator.h @@ -111,6 +111,7 @@ protected: TF_map_glow = 0x080, TF_map_gloss = 0x100, TF_map_emission = 0x001000000, + TF_map_occlusion = 0x002000000, TF_uses_color = 0x200, TF_uses_primary_color = 0x400, TF_uses_last_saved_result = 0x800, diff --git a/pandatool/src/assimp/assimpLoader.cxx b/pandatool/src/assimp/assimpLoader.cxx index 80b94a7ca3..592520df0e 100644 --- a/pandatool/src/assimp/assimpLoader.cxx +++ b/pandatool/src/assimp/assimpLoader.cxx @@ -58,6 +58,10 @@ #define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR "$mat.gltf.pbrMetallicRoughness.roughnessFactor", 0, 0 #endif +#ifndef AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE +#define AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE aiTextureType_UNKNOWN, 0 +#endif + #ifndef AI_MATKEY_GLTF_ALPHAMODE #define AI_MATKEY_GLTF_ALPHAMODE "$mat.gltf.alphaMode", 0, 0 #endif @@ -325,11 +329,13 @@ load_texture(size_t index) { /** * Converts an aiMaterial into a RenderState. + * The dummy argument exists so we can pass a MATKEY macro into this function + * instead of just a texture type. */ void AssimpLoader:: -load_texture_stage(const aiMaterial &mat, const aiTextureType &ttype, - TextureStage::Mode mode, CPT(TextureAttrib) &tattr, - CPT(TexMatrixAttrib) &tmattr) { +load_texture_stage(const aiMaterial &mat, TextureStage::Mode mode, + CPT(TextureAttrib) &tattr, CPT(TexMatrixAttrib) &tmattr, + const aiTextureType &ttype, unsigned int dummy) { aiString path; aiTextureMapping mapping; unsigned int uvindex; @@ -596,20 +602,33 @@ load_material(size_t index) { // And let's not forget the textures! CPT(TextureAttrib) tattr = DCAST(TextureAttrib, TextureAttrib::make()); CPT(TexMatrixAttrib) tmattr; - load_texture_stage(mat, aiTextureType_DIFFUSE, TextureStage::M_modulate, tattr, tmattr); + load_texture_stage(mat, TextureStage::M_modulate, tattr, tmattr, aiTextureType_DIFFUSE); - // Check for an ORM map, from the glTF/OBJ importer. glTF also puts it in the - // LIGHTMAP slot, despite only having the lightmap in the red channel, so we - // have to ignore it. - if (mat.GetTextureCount(aiTextureType_UNKNOWN) > 0) { - load_texture_stage(mat, aiTextureType_UNKNOWN, TextureStage::M_selector, tattr, tmattr); + // Check for an ORM map, from the glTF/OBJ importer. glTF also erroneously + // puts an occlusion texture in the LIGHTMAP slot. + aiString roughness_path, occlusion_path; + aiTextureMapping roughness_mapping, occlusion_mapping; + unsigned int roughness_uv, occlusion_uv; + if (AI_SUCCESS == mat.GetTexture(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, &roughness_path, &roughness_mapping, &roughness_uv)) { + if (AI_SUCCESS == mat.GetTexture(aiTextureType_LIGHTMAP, 0, &occlusion_path, &occlusion_mapping, &occlusion_uv)) { + if (roughness_path == occlusion_path && + roughness_mapping == occlusion_mapping && + roughness_uv == occlusion_uv) { + load_texture_stage(mat, TextureStage::M_occlusion_metallic_roughness, tattr, tmattr, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE); + } else { + load_texture_stage(mat, TextureStage::M_metallic_roughness, tattr, tmattr, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE); + load_texture_stage(mat, TextureStage::M_occlusion, tattr, tmattr, aiTextureType_LIGHTMAP); + } + } else { + load_texture_stage(mat, TextureStage::M_metallic_roughness, tattr, tmattr, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE); + } } else { - load_texture_stage(mat, aiTextureType_LIGHTMAP, TextureStage::M_modulate, tattr, tmattr); + load_texture_stage(mat, TextureStage::M_modulate, tattr, tmattr, aiTextureType_LIGHTMAP); } - load_texture_stage(mat, aiTextureType_NORMALS, TextureStage::M_normal, tattr, tmattr); - load_texture_stage(mat, aiTextureType_EMISSIVE, TextureStage::M_emission, tattr, tmattr); - load_texture_stage(mat, aiTextureType_HEIGHT, TextureStage::M_height, tattr, tmattr); + load_texture_stage(mat, TextureStage::M_normal, tattr, tmattr, aiTextureType_NORMALS); + load_texture_stage(mat, TextureStage::M_emission, tattr, tmattr, aiTextureType_EMISSIVE); + load_texture_stage(mat, TextureStage::M_height, tattr, tmattr, aiTextureType_HEIGHT); if (tattr->get_num_on_stages() > 0) { state = state->add_attrib(tattr); } diff --git a/pandatool/src/assimp/assimpLoader.h b/pandatool/src/assimp/assimpLoader.h index e323cb1e94..e36e146a23 100644 --- a/pandatool/src/assimp/assimpLoader.h +++ b/pandatool/src/assimp/assimpLoader.h @@ -78,9 +78,9 @@ private: const aiNode *find_node(const aiNode &root, const aiString &name); void load_texture(size_t index); - void load_texture_stage(const aiMaterial &mat, const aiTextureType &ttype, - TextureStage::Mode mode, CPT(TextureAttrib) &tattr, - CPT(TexMatrixAttrib) &tmattr); + void load_texture_stage(const aiMaterial &mat, TextureStage::Mode mode, + CPT(TextureAttrib) &tattr, CPT(TexMatrixAttrib) &tmattr, + const aiTextureType &ttype, unsigned int dummy=0); void load_material(size_t index); void create_joint(Character *character, CharacterJointBundle *bundle, PartGroup *parent, const aiNode &node); void create_anim_channel(const aiAnimation &anim, AnimBundle *bundle, AnimGroup *parent, const aiNode &node); diff --git a/tests/display/test_color_buffer.py b/tests/display/test_color_buffer.py index 1450981f37..e2a43991ad 100644 --- a/tests/display/test_color_buffer.py +++ b/tests/display/test_color_buffer.py @@ -367,3 +367,31 @@ def test_color_transparency_no_light(color_region, shader_attrib): ) result = render_color_pixel(color_region, state) assert result.x == pytest.approx(1.0, 0.1) + + +def test_texture_occlusion(color_region): + shader_attrib = core.ShaderAttrib.make_default().set_shader_auto(True) + + tex = core.Texture("occlusion") + tex.set_clear_color((0.5, 1.0, 1.0, 1.0)) + stage = core.TextureStage("occlusion") + stage.set_mode(core.TextureStage.M_occlusion) + texture_attrib = core.TextureAttrib.make().add_on_stage(stage, tex) + + mat = core.Material() + mat.diffuse = (0, 1, 0, 1) + mat.ambient = (0, 0, 1, 1) + material_attrib = core.MaterialAttrib.make(mat) + + alight = core.AmbientLight("ambient") + alight.set_color((0, 0, 1, 1)) + light_attrib = core.LightAttrib.make().add_on_light(core.NodePath(alight)) + + state = core.RenderState.make( + shader_attrib, + material_attrib, + texture_attrib, + light_attrib + ) + result = render_color_pixel(color_region, state) + assert result.z == pytest.approx(0.5, 0.05) From df4ebca33556565f2799d0b8c662281618dd0015 Mon Sep 17 00:00:00 2001 From: a2cy <75477334+a2cy@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:58:19 +0100 Subject: [PATCH 220/291] audiotraits: Add support for different coordinate systems for 3d audio Backport of 4705b329436bdb9717261eb97f3163d1155dae53 (#1786) --- panda/src/audiotraits/openalAudioManager.cxx | 176 ++++++++++++++++--- panda/src/audiotraits/openalAudioSound.cxx | 53 +++++- 2 files changed, 199 insertions(+), 30 deletions(-) diff --git a/panda/src/audiotraits/openalAudioManager.cxx b/panda/src/audiotraits/openalAudioManager.cxx index 59ce773cb1..54f54f83a6 100644 --- a/panda/src/audiotraits/openalAudioManager.cxx +++ b/panda/src/audiotraits/openalAudioManager.cxx @@ -701,21 +701,85 @@ get_active() const { void OpenALAudioManager:: audio_3d_set_listener_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, PN_stdfloat vx, PN_stdfloat vy, PN_stdfloat vz, PN_stdfloat fx, PN_stdfloat fy, PN_stdfloat fz, PN_stdfloat ux, PN_stdfloat uy, PN_stdfloat uz) { ReMutexHolder holder(_lock); - _position[0] = px; - _position[1] = pz; - _position[2] = -py; + CoordinateSystem cs = get_default_coordinate_system(); - _velocity[0] = vx; - _velocity[1] = vz; - _velocity[2] = -vy; + switch (cs) { + case CS_yup_right: + _position[0] = px; + _position[1] = py; + _position[2] = pz; - _forward_up[0] = fx; - _forward_up[1] = fz; - _forward_up[2] = -fy; + _velocity[0] = vx; + _velocity[1] = vy; + _velocity[2] = vz; - _forward_up[3] = ux; - _forward_up[4] = uz; - _forward_up[5] = -uy; + _forward_up[0] = fx; + _forward_up[1] = fy; + _forward_up[2] = fz; + + _forward_up[3] = ux; + _forward_up[4] = uy; + _forward_up[5] = uz; + break; + + case CS_zup_right: + _position[0] = px; + _position[1] = pz; + _position[2] = -py; + + _velocity[0] = vx; + _velocity[1] = vz; + _velocity[2] = -vy; + + _forward_up[0] = fx; + _forward_up[1] = fz; + _forward_up[2] = -fy; + + _forward_up[3] = ux; + _forward_up[4] = uz; + _forward_up[5] = -uy; + break; + + case CS_yup_left: + _position[0] = px; + _position[1] = py; + _position[2] = -pz; + + _velocity[0] = vx; + _velocity[1] = vy; + _velocity[2] = -vz; + + _forward_up[0] = fx; + _forward_up[1] = fy; + _forward_up[2] = -fz; + + _forward_up[3] = ux; + _forward_up[4] = uy; + _forward_up[5] = -uz; + break; + + case CS_zup_left: + _position[0] = px; + _position[1] = pz; + _position[2] = py; + + _velocity[0] = vx; + _velocity[1] = vz; + _velocity[2] = vy; + + _forward_up[0] = fx; + _forward_up[1] = fz; + _forward_up[2] = fy; + + _forward_up[3] = ux; + _forward_up[4] = uz; + _forward_up[5] = uy; + break; + + default: + nassert_raise("invalid coordinate system"); + return; + } make_current(); @@ -735,21 +799,85 @@ audio_3d_set_listener_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, void OpenALAudioManager:: audio_3d_get_listener_attributes(PN_stdfloat *px, PN_stdfloat *py, PN_stdfloat *pz, PN_stdfloat *vx, PN_stdfloat *vy, PN_stdfloat *vz, PN_stdfloat *fx, PN_stdfloat *fy, PN_stdfloat *fz, PN_stdfloat *ux, PN_stdfloat *uy, PN_stdfloat *uz) { ReMutexHolder holder(_lock); - *px = _position[0]; - *py = -_position[2]; - *pz = _position[1]; + CoordinateSystem cs = get_default_coordinate_system(); - *vx = _velocity[0]; - *vy = -_velocity[2]; - *vz = _velocity[1]; + switch (cs) { + case CS_yup_right: + *px = _position[0]; + *py = _position[1]; + *pz = _position[2]; - *fx = _forward_up[0]; - *fy = -_forward_up[2]; - *fz = _forward_up[1]; + *vx = _velocity[0]; + *vy = _velocity[1]; + *vz = _velocity[2]; - *ux = _forward_up[3]; - *uy = -_forward_up[5]; - *uz = _forward_up[4]; + *fx = _forward_up[0]; + *fy = _forward_up[1]; + *fz = _forward_up[2]; + + *ux = _forward_up[3]; + *uy = _forward_up[4]; + *uz = _forward_up[5]; + break; + + case CS_zup_right: + *px = _position[0]; + *py = -_position[2]; + *pz = _position[1]; + + *vx = _velocity[0]; + *vy = -_velocity[2]; + *vz = _velocity[1]; + + *fx = _forward_up[0]; + *fy = -_forward_up[2]; + *fz = _forward_up[1]; + + *ux = _forward_up[3]; + *uy = -_forward_up[5]; + *uz = _forward_up[4]; + break; + + case CS_yup_left: + *px = _position[0]; + *py = _position[1]; + *pz = -_position[2]; + + *vx = _velocity[0]; + *vy = _velocity[1]; + *vz = -_velocity[2]; + + *fx = _forward_up[0]; + *fy = _forward_up[1]; + *fz = -_forward_up[2]; + + *ux = _forward_up[3]; + *uy = _forward_up[4]; + *uz = -_forward_up[5]; + break; + + case CS_zup_left: + *px = _position[0]; + *py = _position[2]; + *pz = _position[1]; + + *vx = _velocity[0]; + *vy = _velocity[2]; + *vz = _velocity[1]; + + *fx = _forward_up[0]; + *fy = _forward_up[2]; + *fz = _forward_up[1]; + + *ux = _forward_up[3]; + *uy = _forward_up[5]; + *uz = _forward_up[4]; + break; + + default: + nassert_raise("invalid coordinate system"); + return; + } } /** diff --git a/panda/src/audiotraits/openalAudioSound.cxx b/panda/src/audiotraits/openalAudioSound.cxx index 155d6211fb..335090f32a 100644 --- a/panda/src/audiotraits/openalAudioSound.cxx +++ b/panda/src/audiotraits/openalAudioSound.cxx @@ -14,6 +14,7 @@ // Panda Headers #include "throw_event.h" +#include "coordinateSystem.h" #include "openalAudioSound.h" #include "openalAudioManager.h" @@ -692,13 +693,53 @@ length() const { void OpenALAudioSound:: set_3d_attributes(PN_stdfloat px, PN_stdfloat py, PN_stdfloat pz, PN_stdfloat vx, PN_stdfloat vy, PN_stdfloat vz) { ReMutexHolder holder(OpenALAudioManager::_lock); - _location[0] = px; - _location[1] = pz; - _location[2] = -py; + CoordinateSystem cs = get_default_coordinate_system(); - _velocity[0] = vx; - _velocity[1] = vz; - _velocity[2] = -vy; + switch (cs) { + case CS_yup_right: + _location[0] = px; + _location[1] = py; + _location[2] = pz; + + _velocity[0] = vx; + _velocity[1] = vy; + _velocity[2] = vz; + break; + + case CS_zup_right: + _location[0] = px; + _location[1] = pz; + _location[2] = -py; + + _velocity[0] = vx; + _velocity[1] = vz; + _velocity[2] = -vy; + break; + + case CS_yup_left: + _location[0] = px; + _location[1] = py; + _location[2] = -pz; + + _velocity[0] = vx; + _velocity[1] = vy; + _velocity[2] = -vz; + break; + + case CS_zup_left: + _location[0] = px; + _location[1] = pz; + _location[2] = py; + + _velocity[0] = vx; + _velocity[1] = vz; + _velocity[2] = vy; + break; + + default: + nassert_raise("invalid coordinate system"); + return; + } if (is_playing()) { _manager->make_current(); From c8c5ae829d13d2eab8910c2c31099b632ce5a8a0 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 14 Nov 2024 20:07:48 +0100 Subject: [PATCH 221/291] putil: Fix SparseArray get_lowest_on_bit and get_lowest_off_bit Cherry-pick of d173086a6f25ffe238f90b65386dad54119eb618 --- panda/src/putil/sparseArray.cxx | 14 +++++++++++--- tests/putil/test_sparsearray.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/panda/src/putil/sparseArray.cxx b/panda/src/putil/sparseArray.cxx index 5404728aa1..54aa8b03ea 100644 --- a/panda/src/putil/sparseArray.cxx +++ b/panda/src/putil/sparseArray.cxx @@ -89,12 +89,16 @@ get_num_off_bits() const { /** * Returns the index of the lowest 1 bit in the array. Returns -1 if there - * are no 1 bits or if there are an infinite number of 1 bits. + * are no 1 bits and 0 if there are an infinite number of 1 bits. */ int SparseArray:: get_lowest_on_bit() const { if (_inverse) { - return -1; + if (_subranges.empty() || _subranges[0]._begin > 0) { + return 0; + } else { + return _subranges[0]._end; + } } if (_subranges.empty()) { @@ -111,7 +115,11 @@ get_lowest_on_bit() const { int SparseArray:: get_lowest_off_bit() const { if (!_inverse) { - return -1; + if (_subranges.empty() || _subranges[0]._begin > 0) { + return 0; + } else { + return _subranges[0]._end; + } } if (_subranges.empty()) { diff --git a/tests/putil/test_sparsearray.py b/tests/putil/test_sparsearray.py index 5138914cdf..4c627a29be 100644 --- a/tests/putil/test_sparsearray.py +++ b/tests/putil/test_sparsearray.py @@ -235,6 +235,35 @@ def test_sparse_array_augm_assignment(): assert s is t +def test_sparse_array_lowest_bit(): + sa = core.SparseArray() + assert sa.get_lowest_off_bit() == 0 + assert sa.get_lowest_on_bit() == -1 + + sa.invert_in_place() + assert sa.get_lowest_off_bit() == -1 + assert sa.get_lowest_on_bit() == 0 + + sa = core.SparseArray() + sa.set_bit(0) + sa.set_bit(1) + assert sa.get_lowest_off_bit() == 2 + assert sa.get_lowest_on_bit() == 0 + + sa.invert_in_place() + assert sa.get_lowest_off_bit() == 0 + assert sa.get_lowest_on_bit() == 2 + + sa = core.SparseArray() + sa.set_bit(2) + assert sa.get_lowest_off_bit() == 0 + assert sa.get_lowest_on_bit() == 2 + + sa.invert_in_place() + assert sa.get_lowest_off_bit() == 2 + assert sa.get_lowest_on_bit() == 0 + + def test_sparse_array_getstate(): sa = core.SparseArray() assert sa.__getstate__() == () From d020b25afe0527397ef98a1d4e68c7f73c772523 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 14 Nov 2024 20:21:04 +0100 Subject: [PATCH 222/291] windisplay: Fix recursive loop in adjust_z_order Cherry-pick of 639497310e2972eae81ee825dbdff14cdc02c520 with alteration to preserve ABI (replacing an unused bool field) --- panda/src/windisplay/winGraphicsWindow.cxx | 11 ++++++++++- panda/src/windisplay/winGraphicsWindow.h | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/panda/src/windisplay/winGraphicsWindow.cxx b/panda/src/windisplay/winGraphicsWindow.cxx index 0a216be367..c7f8afd26d 100644 --- a/panda/src/windisplay/winGraphicsWindow.cxx +++ b/panda/src/windisplay/winGraphicsWindow.cxx @@ -1308,6 +1308,11 @@ adjust_z_order() { void WinGraphicsWindow:: adjust_z_order(WindowProperties::ZOrder last_z_order, WindowProperties::ZOrder this_z_order) { + // Prevent calling this recursively. + if (_in_adjust_z_order) { + return; + } + HWND order; bool do_change = false; @@ -1337,8 +1342,10 @@ adjust_z_order(WindowProperties::ZOrder last_z_order, break; } if (do_change) { + _in_adjust_z_order = true; BOOL result = SetWindowPos(_hWnd, order, 0,0,0,0, SWP_NOMOVE | SWP_NOSENDCHANGING | SWP_NOSIZE); + _in_adjust_z_order = false; if (!result) { windisplay_cat.warning() << "SetWindowPos failed.\n"; @@ -1630,7 +1637,9 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { if (_hWnd != nullptr) { handle_reshape(); } - adjust_z_order(); + if (!_in_adjust_z_order) { + adjust_z_order(); + } return 0; case WM_PAINT: diff --git a/panda/src/windisplay/winGraphicsWindow.h b/panda/src/windisplay/winGraphicsWindow.h index a0416a873f..1f1edaac97 100644 --- a/panda/src/windisplay/winGraphicsWindow.h +++ b/panda/src/windisplay/winGraphicsWindow.h @@ -173,7 +173,7 @@ private: bool _ime_open; bool _ime_active; bool _tracking_mouse_leaving; - bool _bCursor_in_WindowClientArea; + bool _in_adjust_z_order = false; HANDLE _input_device_handle[32]; HCURSOR _cursor; DEVMODE _fullscreen_display_mode; From cbd25a4d802afa7389df4a51e6964c0f4f4a64a6 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Nov 2024 16:40:49 +0100 Subject: [PATCH 223/291] makepanda: Don't look for macOS framework when cross-compiling Backport of 7191fb1ed68e470cdc7b11f3b5b74544991caea0 --- makepanda/makepandacore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 74a4307a29..4df2de0e78 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -1831,7 +1831,7 @@ def SmartPkgEnable(pkg, pkgconfig = None, libs = None, incs = None, defs = None, DefSymbol(target_pkg, d, v) return - elif not custom_loc and GetHost() == "darwin" and framework != None: + elif not custom_loc and GetHost() == "darwin" and GetTarget() == "darwin" and framework is not None: prefix = SDK["MACOSX"] if (os.path.isdir(prefix + "/Library/Frameworks/%s.framework" % framework) or os.path.isdir(prefix + "/System/Library/Frameworks/%s.framework" % framework) or From 6b3ed01756086251d5070fb0a67bc046559da958 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 26 Nov 2024 18:16:01 +0100 Subject: [PATCH 224/291] framework: Clean up task manager on close_framework() This prevents crashes due to the tasks it spawns continuing to run Backport of a32030d46e6a96547fe4ebe6b1036be652ef659f --- panda/src/framework/pandaFramework.cxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/panda/src/framework/pandaFramework.cxx b/panda/src/framework/pandaFramework.cxx index 706ff4995d..29f5746c38 100644 --- a/panda/src/framework/pandaFramework.cxx +++ b/panda/src/framework/pandaFramework.cxx @@ -189,6 +189,8 @@ close_framework() { _event_handler.remove_all_hooks(); + _task_mgr.cleanup(); + _is_open = false; _made_default_pipe = false; _default_pipe.clear(); From 40ecb01bb157badac4fd482b8806721d4e3ad0c4 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 14 Dec 2024 12:50:02 +0100 Subject: [PATCH 225/291] samples: Remove unused CardMaker import from procedural-cube sample Backport of 5d465be01e9ac5a8aad9cb689af822f60682b7e9 --- samples/procedural-cube/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/procedural-cube/main.py b/samples/procedural-cube/main.py index 2f21cd9a0f..266afc4205 100755 --- a/samples/procedural-cube/main.py +++ b/samples/procedural-cube/main.py @@ -17,7 +17,6 @@ from panda3d.core import GeomVertexFormat, GeomVertexData from panda3d.core import Geom, GeomTriangles, GeomVertexWriter from panda3d.core import Texture, GeomNode from panda3d.core import PerspectiveLens -from panda3d.core import CardMaker from panda3d.core import Light, Spotlight from panda3d.core import TextNode from panda3d.core import LVector3 From b09ebf3b84ae9d3f858edee19871bce450072d12 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 29 Dec 2024 14:39:43 +0100 Subject: [PATCH 226/291] makewheel: Exclude some more system libs Backport of be382a8f29f81c289be818760eae12523aa26837 --- makepanda/makewheel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/makepanda/makewheel.py b/makepanda/makewheel.py index c0fe650fdf..e60033b9a3 100644 --- a/makepanda/makewheel.py +++ b/makepanda/makewheel.py @@ -114,7 +114,9 @@ MANYLINUX_LIBS = [ # These are not mentioned in manylinux1 spec but should nonetheless always # be excluded. "linux-vdso.so.1", "linux-gate.so.1", "ld-linux.so.2", "libdrm.so.2", + "ld-linux-x86-64.so.2", "ld-linux-aarch64.so.1", "libEGL.so.1", "libOpenGL.so.0", "libGLX.so.0", "libGLdispatch.so.0", + "libGLESv2.so.2", ] # Binaries to never scan for dependencies on non-Windows systems. From 33888de3b54311b697b7179b955925c98b33a05c Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 29 Jan 2025 11:12:07 +0100 Subject: [PATCH 227/291] glgsg: Do not prefer depth-stencil ms renderbuffers on macOS Fixes #1719 Backport of fa64a52fca875b99c1cdb423968bb53985afec7b --- panda/src/glstuff/glGraphicsBuffer_src.cxx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/panda/src/glstuff/glGraphicsBuffer_src.cxx b/panda/src/glstuff/glGraphicsBuffer_src.cxx index e63ea89b17..f6444806df 100644 --- a/panda/src/glstuff/glGraphicsBuffer_src.cxx +++ b/panda/src/glstuff/glGraphicsBuffer_src.cxx @@ -517,6 +517,18 @@ rebuild_bitplanes() { // but it's a waste. Let's not do it unless the user requested stencil. _use_depth_stencil = false; +#ifdef __APPLE__ + } else if (_fb_properties.get_depth_bits() > 0 && _requested_multisamples) { + // Apple's OpenGL driver doesn't like blitting depth-stencil targets. + // See GitHub issue #1719 + _use_depth_stencil = false; + if (_fb_properties.get_depth_bits() < 24) { + // Make sure we do get at least as many depth bits as we would have + // gotten if we did get a depth-stencil buffer. + _fb_properties.set_depth_bits(24); + } +#endif + } else if (_fb_properties.get_depth_bits() > 0) { // Let's use a depth stencil buffer by default, if a depth buffer was // requested. From e319be1fcb226d9aabd33ada77f3a317d6854c12 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 12:17:55 +0100 Subject: [PATCH 228/291] doc: Add release notes for 1.10.16 --- doc/ReleaseNotes | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/doc/ReleaseNotes b/doc/ReleaseNotes index e5651145b7..6374795a59 100644 --- a/doc/ReleaseNotes +++ b/doc/ReleaseNotes @@ -1,3 +1,29 @@ +----------------------- RELEASE 1.10.16 ----------------------- + +This maintenance release fixes some minor defects and stability issues. + +* OpenAL: Support non-default coordinate systems when playing 3D audio +* Tasks: Coroutine detect now also handles coroutine subclass to support Nuitka +* Tasks: Now properly handles generators without send() +* Windows: Fixes a hang when adjusting Z-order in some situations +* Fix SparseArray methods get_lowest_off_bit() and get_lowest_on_bit() +* Fix linecache error when distributing for newer Python versions +* Fix `await AsyncFuture.gather()` returning first item instead of tuple (#1738) +* Fix use-after-free in collision system with transform cache disabled (#1733) +* OpenGL: fix error blitting depth texture on macOS (#1719) +* OpenGL: fix SSBO support not being detected in certain situations +* X11: Add config variable to enable detection of autorepeat key events (#1735) +* GUI: Fix bug with PGSliderBar dragging (#1722) +* Minor thread safety things for free-threaded Python builds +* Add forward compatibility for bam version 6.46 +* Fixes a harmless buffer overflow in pdtoa +* Fix compilation issues with SDL version of tinydisplay (#1708) +* bam2egg: Fix issue when having more than two tags (#1725) +* Fix "Detected leak for ... which interrogate cannot delete." error (#1743) +* Fix PythonCallbackObject crash upon destruction in some cases +* PStats: Fix crash when receiving frames out of order +* PandaFramework::close_framework() now clears task manager of tasks + ----------------------- RELEASE 1.10.15 ----------------------- This release adds support for Python 3.13, and fixes some significant bugs. From 975a9fb484f56db32ba13ca4770939cee0388fd2 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 12:41:34 +0100 Subject: [PATCH 229/291] egg: Add limited forward compatibility for ORM textures Mainly intended to be able to passthrough 1.11 egg files safely --- doc/ReleaseNotes | 1 + panda/src/egg/eggTexture.cxx | 23 +++++++++++++++++++++++ panda/src/egg/eggTexture.h | 5 +++++ panda/src/egg2pg/eggLoader.cxx | 6 ++++++ 4 files changed, 35 insertions(+) diff --git a/doc/ReleaseNotes b/doc/ReleaseNotes index 6374795a59..a9322f8518 100644 --- a/doc/ReleaseNotes +++ b/doc/ReleaseNotes @@ -10,6 +10,7 @@ This maintenance release fixes some minor defects and stability issues. * Fix linecache error when distributing for newer Python versions * Fix `await AsyncFuture.gather()` returning first item instead of tuple (#1738) * Fix use-after-free in collision system with transform cache disabled (#1733) +* Egg: Add limited forward compatibility for metallic-roughness textures * OpenGL: fix error blitting depth texture on macOS (#1719) * OpenGL: fix SSBO support not being detected in certain situations * X11: Add config variable to enable detection of autorepeat key events (#1735) diff --git a/panda/src/egg/eggTexture.cxx b/panda/src/egg/eggTexture.cxx index 899438276e..4fb9ba1c3e 100644 --- a/panda/src/egg/eggTexture.cxx +++ b/panda/src/egg/eggTexture.cxx @@ -564,6 +564,11 @@ affects_polygon_alpha() const { case ET_selector: return true; + case ET___occlusion: + case ET___occlusion_metallic_roughness: + case ET___metallic_roughness: + return false; + case ET_unspecified: break; } @@ -886,6 +891,15 @@ string_env_type(const string &string) { } else if (cmp_nocase_uh(string, "emission") == 0) { return ET_emission; + } else if (cmp_nocase_uh(string, "occlusion") == 0) { + return ET___occlusion; + + } else if (cmp_nocase_uh(string, "occlusion_metallic_roughness") == 0) { + return ET___occlusion_metallic_roughness; + + } else if (cmp_nocase_uh(string, "metallic_roughness") == 0) { + return ET___metallic_roughness; + } else { return ET_unspecified; } @@ -1319,6 +1333,15 @@ ostream &operator << (ostream &out, EggTexture::EnvType type) { case EggTexture::ET_emission: return out << "emission"; + + case EggTexture::ET___occlusion: + return out << "occlusion"; + + case EggTexture::ET___occlusion_metallic_roughness: + return out << "occlusion_metallic_roughness"; + + case EggTexture::ET___metallic_roughness: + return out << "metallic_roughness"; } nassertr(false, out); diff --git a/panda/src/egg/eggTexture.h b/panda/src/egg/eggTexture.h index 5d602e26f8..6e1b7017fb 100644 --- a/panda/src/egg/eggTexture.h +++ b/panda/src/egg/eggTexture.h @@ -108,6 +108,11 @@ PUBLISHED: ET_selector, ET_normal_gloss, ET_emission, + + // Forward compatibility with Panda 1.11 + ET___occlusion, + ET___occlusion_metallic_roughness, + ET___metallic_roughness, }; enum CombineMode { CM_unspecified, diff --git a/panda/src/egg2pg/eggLoader.cxx b/panda/src/egg2pg/eggLoader.cxx index b1beac16d9..c0d9a7fa55 100644 --- a/panda/src/egg2pg/eggLoader.cxx +++ b/panda/src/egg2pg/eggLoader.cxx @@ -876,6 +876,10 @@ load_textures() { */ bool EggLoader:: load_texture(TextureDef &def, EggTexture *egg_tex) { + if (egg_tex->get_env_type() == EggTexture::ET___occlusion) { + return false; + } + // Check to see if we should reduce the number of channels in the texture. int wanted_channels = 0; bool wanted_alpha = false; @@ -1533,6 +1537,8 @@ make_texture_stage(const EggTexture *egg_tex) { break; case EggTexture::ET_selector: + case EggTexture::ET___metallic_roughness: + case EggTexture::ET___occlusion_metallic_roughness: stage->set_mode(TextureStage::M_selector); break; From 64b98ea81371e230907f10c19a9ea3e7f3d75535 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 12:43:21 +0100 Subject: [PATCH 230/291] GLSL: Add p3d_MetallicRoughnessTexture input mapped to M_metallic_roughness This new M_metallic_roughness enum value is just an alias of M_selector --- doc/ReleaseNotes | 1 + panda/src/glstuff/glShaderContext_src.cxx | 3 ++- panda/src/gobj/textureStage.h | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/ReleaseNotes b/doc/ReleaseNotes index a9322f8518..5942c69e8c 100644 --- a/doc/ReleaseNotes +++ b/doc/ReleaseNotes @@ -13,6 +13,7 @@ This maintenance release fixes some minor defects and stability issues. * Egg: Add limited forward compatibility for metallic-roughness textures * OpenGL: fix error blitting depth texture on macOS (#1719) * OpenGL: fix SSBO support not being detected in certain situations +* GLSL: Add p3d_MetallicRoughnessTexture input mapped to M_metallic_roughness * X11: Add config variable to enable detection of autorepeat key events (#1735) * GUI: Fix bug with PGSliderBar dragging (#1722) * Minor thread safety things for free-threaded Python builds diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx index 1de385095f..6a65fb1f8d 100644 --- a/panda/src/glstuff/glShaderContext_src.cxx +++ b/panda/src/glstuff/glShaderContext_src.cxx @@ -1015,7 +1015,8 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) { else if (noprefix.compare(7, string::npos, "Height") == 0) { bind._part = Shader::STO_stage_height_i; } - else if (noprefix.compare(7, string::npos, "Selector") == 0) { + else if (noprefix.compare(7, string::npos, "MetallicRoughness") == 0 || + noprefix.compare(7, string::npos, "Selector") == 0) { bind._part = Shader::STO_stage_selector_i; } else if (noprefix.compare(7, string::npos, "Gloss") == 0) { diff --git a/panda/src/gobj/textureStage.h b/panda/src/gobj/textureStage.h index ebbd1ab1a2..ce15745457 100644 --- a/panda/src/gobj/textureStage.h +++ b/panda/src/gobj/textureStage.h @@ -65,6 +65,9 @@ PUBLISHED: M_normal_gloss, M_emission, + + // Forward compatibility alias. + M_metallic_roughness = M_selector, }; enum CombineMode { From f123cb142d68022759227cf2876a168bf6aa21c1 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 15:59:06 +0100 Subject: [PATCH 231/291] shader: Fix memory corruption with matrix array shader inputs Fixes #1788 --- panda/src/gobj/shader.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/gobj/shader.cxx b/panda/src/gobj/shader.cxx index fc4ac4e7ad..8cea76df6c 100644 --- a/panda/src/gobj/shader.cxx +++ b/panda/src/gobj/shader.cxx @@ -742,7 +742,7 @@ cp_add_mat_spec(ShaderMatSpec &spec) { spec._cache_offset[p] = offset + begin[p] * size; } if (spec._func == SMF_shader_input_ptr) { - _mat_scratch_size = std::max(_mat_scratch_size, spec._array_count); + _mat_scratch_size = std::max(_mat_scratch_size, spec._array_count * ((spec._num_components + 3) / 4)); // We specify SSD_frame because a PTA may be modified by the app from // frame to frame, and we have no way to know. So, we must respecify a From 14b8cd74c25f08ec6b3614315d1c73a047d41347 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 16:16:31 +0100 Subject: [PATCH 232/291] tests: Fix GLSL shadow matrix unit test values --- tests/display/test_glsl_shader.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/display/test_glsl_shader.py b/tests/display/test_glsl_shader.py index a49c9a0468..d6dd9ff5a5 100644 --- a/tests/display/test_glsl_shader.py +++ b/tests/display/test_glsl_shader.py @@ -615,16 +615,16 @@ def test_glsl_state_light_source(gsg): assert(p3d_LightSource[0].shadowViewMatrix[0][1] == 0); assert(p3d_LightSource[0].shadowViewMatrix[0][2] == 0); assert(p3d_LightSource[0].shadowViewMatrix[0][3] == 0); - assert(p3d_LightSource[0].shadowViewMatrix[1][0] == 0); - assert(p3d_LightSource[0].shadowViewMatrix[1][1] > 0.2886); - assert(p3d_LightSource[0].shadowViewMatrix[1][1] < 0.2887); - assert(p3d_LightSource[0].shadowViewMatrix[1][2] == 0); - assert(p3d_LightSource[0].shadowViewMatrix[1][3] == 0); - assert(p3d_LightSource[0].shadowViewMatrix[2][0] == -0.5); - assert(p3d_LightSource[0].shadowViewMatrix[2][1] == -0.5); - assert(p3d_LightSource[0].shadowViewMatrix[2][2] > -1.00002); - assert(p3d_LightSource[0].shadowViewMatrix[2][2] < -1.0); - assert(p3d_LightSource[0].shadowViewMatrix[2][3] == -1); + assert(p3d_LightSource[0].shadowViewMatrix[1][0] == 0.5); + assert(p3d_LightSource[0].shadowViewMatrix[1][1] == 0.5); + assert(p3d_LightSource[0].shadowViewMatrix[1][2] > 1.0); + assert(p3d_LightSource[0].shadowViewMatrix[1][2] < 1.00002); + assert(p3d_LightSource[0].shadowViewMatrix[1][3] == 1); + assert(p3d_LightSource[0].shadowViewMatrix[2][0] == 0); + assert(p3d_LightSource[0].shadowViewMatrix[2][1] > 0.2886); + assert(p3d_LightSource[0].shadowViewMatrix[2][1] < 0.2887); + assert(p3d_LightSource[0].shadowViewMatrix[2][2] == 0); + assert(p3d_LightSource[0].shadowViewMatrix[2][3] == 0); assert(p3d_LightSource[0].shadowViewMatrix[3][0] > -16.2736); assert(p3d_LightSource[0].shadowViewMatrix[3][0] < -16.2734); assert(p3d_LightSource[0].shadowViewMatrix[3][1] > -16.8510); From 86d0376054173b993a3d329a36fb7e994a12b880 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 22:20:57 +0100 Subject: [PATCH 233/291] glgsg: Mark SSBO unbound from all binding points upon deletion Fixes a consistency issue where the buffer might not be rebound properly --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index ec5ca7ad2c..034b5e5434 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -7692,6 +7692,12 @@ release_shader_buffer(BufferContext *bc) { _current_sbuffer_index = 0; } + for (GLuint &index : _current_sbuffer_base) { + if (index == gbc->_index) { + index = 0; + } + } + _glDeleteBuffers(1, &gbc->_index); report_my_gl_errors(); @@ -7731,6 +7737,12 @@ release_shader_buffers(const pvector &contexts) { _current_sbuffer_index = 0; } + for (GLuint &index : _current_sbuffer_base) { + if (index == gbc->_index) { + index = 0; + } + } + if (debug) { GLCAT.debug() << "deleting shader buffer " << gbc->_index << "\n"; From 225eb9b34dd8b91f4eca004b9806e179256d192b Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 22 Dec 2025 22:20:57 +0100 Subject: [PATCH 234/291] glgsg: Mark SSBO unbound from all binding points upon deletion Fixes a consistency issue where the buffer might not be rebound properly Backport of 86d0376054173b993a3d329a36fb7e994a12b880 --- doc/ReleaseNotes | 1 + panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/doc/ReleaseNotes b/doc/ReleaseNotes index 5942c69e8c..d0769cd785 100644 --- a/doc/ReleaseNotes +++ b/doc/ReleaseNotes @@ -12,6 +12,7 @@ This maintenance release fixes some minor defects and stability issues. * Fix use-after-free in collision system with transform cache disabled (#1733) * Egg: Add limited forward compatibility for metallic-roughness textures * OpenGL: fix error blitting depth texture on macOS (#1719) +* OpenGL: fix SSBO not being rebound if deleted and recreated immediately * OpenGL: fix SSBO support not being detected in certain situations * GLSL: Add p3d_MetallicRoughnessTexture input mapped to M_metallic_roughness * X11: Add config variable to enable detection of autorepeat key events (#1735) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index a11e03377e..12e55cdf9f 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -6837,6 +6837,12 @@ release_shader_buffer(BufferContext *bc) { _current_sbuffer_index = 0; } + for (GLuint &index : _current_sbuffer_base) { + if (index == gbc->_index) { + index = 0; + } + } + _glDeleteBuffers(1, &gbc->_index); report_my_gl_errors(); From 8177021155dc0b945831ed02455e11612212dbc1 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 23 Dec 2025 20:30:56 +0100 Subject: [PATCH 235/291] tests: Fix unit test suite on Python 2.7 --- tests/event/test_futures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/event/test_futures.py b/tests/event/test_futures.py index db016ae8d3..6296d28f6c 100644 --- a/tests/event/test_futures.py +++ b/tests/event/test_futures.py @@ -16,6 +16,9 @@ def check_result(fut, expected): if fut.result() != expected: return False + if sys.version_info < (3, 5): + return False + # Make sure that await also returns the values properly with pytest.raises(StopIteration) as e: next(fut.__await__()) From 12b5fafa9fd64dc4d52005d3eb3e100aa59a74e0 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 23 Dec 2025 20:31:56 +0100 Subject: [PATCH 236/291] tests: Fix future tests always failing on Python pre-3.5 --- tests/event/test_futures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/event/test_futures.py b/tests/event/test_futures.py index 6296d28f6c..de7d51068f 100644 --- a/tests/event/test_futures.py +++ b/tests/event/test_futures.py @@ -17,7 +17,7 @@ def check_result(fut, expected): return False if sys.version_info < (3, 5): - return False + return True # Make sure that await also returns the values properly with pytest.raises(StopIteration) as e: From c6e72fbd4f2aca03ce56b27abc520970ad668e99 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 25 Dec 2025 15:14:40 +0100 Subject: [PATCH 237/291] makepanda: Don't set HAVE_OPENAL_FRAMEWORK when building with thirdparty --- makepanda/makepanda.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 2fd81b6624..ad6230f0ed 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -2766,7 +2766,8 @@ def WriteConfigSettings(): dtool_config["PYTHON_FRAMEWORK"] = 'Python' dtool_config["PHAVE_MALLOC_H"] = 'UNDEF' dtool_config["PHAVE_SYS_MALLOC_H"] = '1' - dtool_config["HAVE_OPENAL_FRAMEWORK"] = '1' + if not os.path.isdir(os.path.join(GetThirdpartyDir(), 'openal')): + dtool_config["HAVE_OPENAL_FRAMEWORK"] = '1' dtool_config["HAVE_X11"] = 'UNDEF' # We might have X11, but we don't need it. dtool_config["IS_LINUX"] = 'UNDEF' dtool_config["HAVE_VIDEO4LINUX"] = 'UNDEF' From 9f13db32844e1a8c0482a1576ecdee0e9392f54f Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 25 Dec 2025 15:38:12 +0100 Subject: [PATCH 238/291] Update version numbers from 1.10.15 to 1.10.16 --- .github/workflows/ci.yml | 8 ++++---- README.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c304c75af..ecc162ed70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,10 @@ jobs: - name: Get thirdparty packages (macOS) if: runner.os == 'macOS' run: | - curl -O https://www.panda3d.org/download/panda3d-1.10.15/panda3d-1.10.15-tools-mac.tar.gz - tar -xf panda3d-1.10.15-tools-mac.tar.gz - mv panda3d-1.10.15/thirdparty thirdparty - rmdir panda3d-1.10.15 + curl -O https://www.panda3d.org/download/panda3d-1.10.16/panda3d-1.10.16-tools-mac.tar.gz + tar -xf panda3d-1.10.16-tools-mac.tar.gz + mv panda3d-1.10.16/thirdparty thirdparty + rmdir panda3d-1.10.16 (cd thirdparty/darwin-libs-a && rm -rf rocket) - name: Set up Python 3.13 diff --git a/README.md b/README.md index 0621692a86..0002e7b21e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Installing Panda3D ================== The latest Panda3D SDK can be downloaded from -[this page](https://www.panda3d.org/download/sdk-1-10-15/). +[this page](https://www.panda3d.org/download/sdk-1-10-16/). If you are familiar with installing Python packages, you can use the following command: @@ -136,7 +136,7 @@ macOS ----- On macOS, you will need to download a set of precompiled thirdparty packages in order to -compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.15/panda3d-1.10.15-tools-mac.tar.gz). +compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.16/panda3d-1.10.16-tools-mac.tar.gz). After placing the thirdparty directory inside the panda3d source directory, you may build Panda3D using a command like the following: From 4732c247fefdabbb417d9b4406208d453cb0e3a1 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 25 Dec 2025 15:39:05 +0100 Subject: [PATCH 239/291] workflow: Add Python 3.14 to test matrix --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecc162ed70..206df4df39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,20 @@ jobs: rmdir panda3d-1.10.16 (cd thirdparty/darwin-libs-a && rm -rf rocket) + - name: Set up Python 3.14 + uses: actions/setup-python@v5 + with: + python-version: '3.14' + - name: Build Python 3.14 + shell: bash + run: | + python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10 --msvc-version=14.3 + - name: Test Python 3.14 + shell: bash + run: | + python -m pip install pytest setuptools + PYTHONPATH=built LD_LIBRARY_PATH=built/lib:$pythonLocation/lib DYLD_LIBRARY_PATH=built/lib python -m pytest + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: From e560eb8da370d86b096d6cb65eafd9ab40879b9a Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 25 Dec 2025 16:40:30 +0100 Subject: [PATCH 240/291] Add Python 3.14 to setup.cfg [skip ci] --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index e8cf51c227..e1c78047c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ classifiers = Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 + Programming Language :: Python :: 3.14 Programming Language :: Python :: Implementation :: CPython Topic :: Games/Entertainment Topic :: Multimedia From e918eff23175a9f25763c5f0ee06906caaebadd6 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 29 Dec 2025 10:55:50 +0100 Subject: [PATCH 241/291] makepanda: Set default MSVC version to 14.3 (2022) --- makepanda/makepanda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index bb2b99becf..2874f619bd 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -310,7 +310,7 @@ def parseopts(args): if GetTarget() == 'windows': if not MSVC_VERSION: print("No MSVC version specified. Defaulting to 14.1 (Visual Studio 2017).") - MSVC_VERSION = (14, 1) + MSVC_VERSION = (14, 3) else: try: MSVC_VERSION = tuple(int(d) for d in MSVC_VERSION.split('.'))[:2] From 585c58198a56a560a6e49222145ff9bd5a31ed25 Mon Sep 17 00:00:00 2001 From: TRIGON69 Date: Sun, 28 Dec 2025 21:32:07 +0530 Subject: [PATCH 242/291] movies: Update FLAC decoder - Modernized FlacAudio implementation using updated dr_flac APIs. - Fixed Interrogate syntax error in dr_flac.h by guarding MSVC-specific __int64 types with !defined(CPPPARSER). - Updated FlacAudioCursor to use PCM frame-based read and seek methods. - Implemented required callbacks (cb_tell_proc) for modern dr_flac support. Fixes #1570 Closes #1791 --- panda/src/movies/dr_flac.h | 13834 +++++++++++++++++++++---- panda/src/movies/flacAudio.h | 2 +- panda/src/movies/flacAudioCursor.cxx | 46 +- 3 files changed, 11790 insertions(+), 2092 deletions(-) diff --git a/panda/src/movies/dr_flac.h b/panda/src/movies/dr_flac.h index 3d0b1cdd91..accc15b302 100644 --- a/panda/src/movies/dr_flac.h +++ b/panda/src/movies/dr_flac.h @@ -1,349 +1,1623 @@ -// Public domain. See "unlicense" statement at the end of this file. -//NB: modified by rdb to use 16-bit instead of 32-bit samples. +/* +FLAC audio decoder. Choice of public domain or MIT-0. See license statements at the end of this file. +dr_flac - v0.13.2 - 2025-12-02 -// ABOUT -// -// This is a simple library for decoding FLAC files. -// -// -// -// USAGE -// -// This is a single-file library. To use it, do something like the following in one .c file. -// #define DR_FLAC_IMPLEMENTATION -// #include "dr_flac.h" -// -// You can then #include this file in other parts of the program as you would with any other header file. To decode audio data, -// do something like the following: -// -// drflac* pFlac = drflac_open_file("MySong.flac"); -// if (pFlac == NULL) { -// ... Failed to open FLAC file ... -// } -// -// int16_t* pSamples = malloc(pFlac->totalSampleCount * sizeof(int16_t)); -// uint64_t numberOfSamplesActuallyRead = drflac_read_s16(pFlac, pFlac->totalSampleCount, pSamples); -// -// ... pSamples now contains the decoded samples as interleaved signed 16-bit PCM ... -// -// The drflac object represents the decoder. It is a transparent type so all the information you need, such as the number of -// channels and the bits per sample, should be directly accessible - just make sure you don't change their values. -// -// You do not need to decode the entire stream in one go - you just specify how many samples you'd like at any given time and -// the decoder will give you as many samples as it can, up to the amount requested. Later on when you need the next batch of -// samples, just call it again. Example: -// -// while (drflac_read_s16(pFlac, chunkSize, pChunkSamples) > 0) { -// do_something(); -// } -// -// You can seek to a specific sample with drflac_seek_to_sample(). The given sample is based on interleaving. So for example, -// if you were to seek to the sample at index 0 in a stereo stream, you'll be seeking to the first sample of the left channel. -// The sample at index 1 will be the first sample of the right channel. The sample at index 2 will be the second sample of the -// left channel, etc. -// -// -// -// OPTIONS -// #define these options before including this file. -// -// #define DR_FLAC_NO_STDIO -// Disable drflac_open_file(). -// -// #define DR_FLAC_NO_WIN32_IO -// Don't use the Win32 API internally for drflac_open_file(). Setting this will force stdio FILE APIs instead. This is -// mainly for testing, but it's left here in case somebody might find use for it. dr_flac will use the Win32 API by -// default. Ignored when DR_FLAC_NO_STDIO is #defined. -// -// #define DR_FLAC_BUFFER_SIZE -// Defines the size of the internal buffer to store data from onRead(). This buffer is used to reduce the number of calls -// back to the client for more data. Larger values means more memory, but better performance. My tests show diminishing -// returns after about 4KB (which is the default). Consider reducing this if you have a very efficient implementation of -// onRead(), or increase it if it's very inefficient. -// -// -// -// QUICK NOTES -// -// - Based on my own tests, the 32-bit build is about about 1.1x-1.25x slower than the reference implementation. The 64-bit -// build is at about parity. -// - This should work fine with valid native FLAC files, but it won't work very well when the STREAMINFO block is unavailable -// and when a stream starts in the middle of a frame. This is something I plan on addressing. -// - Audio data is retrieved as signed 16-bit PCM, regardless of the bits per sample the FLAC stream is encoded as. -// - This has not been tested on big-endian architectures. -// - Rice codes in unencoded binary form (see https://xiph.org/flac/format.html#rice_partition) has not been tested. If anybody -// knows where I can find some test files for this, let me know. -// - Perverse and erroneous files have not been tested. Again, if you know where I can get some test files let me know. -// - dr_flac is not thread-safe, but it's APIs can be called from any thread so long as you do your own synchronization. -// - dr_flac does not currently do any CRC checks. -// - Ogg encapsulation is not supported, but I want to add it at some point. -// -// -// -// TODO -// - Implement a proper test suite. -// - Add support for initializing the decoder without a STREAMINFO block. Build a synthethic test to get support working at at least -// a basic level. -// - Add support for retrieving metadata blocks so applications can retrieve the album art or whatnot. -// - Add support for Ogg encapsulation. +David Reid - mackron@gmail.com + +GitHub: https://github.com/mackron/dr_libs +*/ + +/* +Introduction +============ +dr_flac is a single file library. To use it, do something like the following in one .c file. + + ```c + #define DR_FLAC_IMPLEMENTATION + #include "dr_flac.h" + ``` + +You can then #include this file in other parts of the program as you would with any other header file. To decode audio data, do something like the following: + + ```c + drflac* pFlac = drflac_open_file("MySong.flac", NULL); + if (pFlac == NULL) { + // Failed to open FLAC file + } + + drflac_int32* pSamples = malloc(pFlac->totalPCMFrameCount * pFlac->channels * sizeof(drflac_int32)); + drflac_uint64 numberOfInterleavedSamplesActuallyRead = drflac_read_pcm_frames_s32(pFlac, pFlac->totalPCMFrameCount, pSamples); + ``` + +The drflac object represents the decoder. It is a transparent type so all the information you need, such as the number of channels and the bits per sample, +should be directly accessible - just make sure you don't change their values. Samples are always output as interleaved signed 32-bit PCM. In the example above +a native FLAC stream was opened, however dr_flac has seamless support for Ogg encapsulated FLAC streams as well. + +You do not need to decode the entire stream in one go - you just specify how many samples you'd like at any given time and the decoder will give you as many +samples as it can, up to the amount requested. Later on when you need the next batch of samples, just call it again. Example: + + ```c + while (drflac_read_pcm_frames_s32(pFlac, chunkSizeInPCMFrames, pChunkSamples) > 0) { + do_something(); + } + ``` + +You can seek to a specific PCM frame with `drflac_seek_to_pcm_frame()`. + +If you just want to quickly decode an entire FLAC file in one go you can do something like this: + + ```c + unsigned int channels; + unsigned int sampleRate; + drflac_uint64 totalPCMFrameCount; + drflac_int32* pSampleData = drflac_open_file_and_read_pcm_frames_s32("MySong.flac", &channels, &sampleRate, &totalPCMFrameCount, NULL); + if (pSampleData == NULL) { + // Failed to open and decode FLAC file. + } + + ... + + drflac_free(pSampleData, NULL); + ``` + +You can read samples as signed 16-bit integer and 32-bit floating-point PCM with the *_s16() and *_f32() family of APIs respectively, but note that these +should be considered lossy. + + +If you need access to metadata (album art, etc.), use `drflac_open_with_metadata()`, `drflac_open_file_with_metdata()` or `drflac_open_memory_with_metadata()`. +The rationale for keeping these APIs separate is that they're slightly slower than the normal versions and also just a little bit harder to use. dr_flac +reports metadata to the application through the use of a callback, and every metadata block is reported before `drflac_open_with_metdata()` returns. + +The main opening APIs (`drflac_open()`, etc.) will fail if the header is not present. The presents a problem in certain scenarios such as broadcast style +streams or internet radio where the header may not be present because the user has started playback mid-stream. To handle this, use the relaxed APIs: + + `drflac_open_relaxed()` + `drflac_open_with_metadata_relaxed()` + +It is not recommended to use these APIs for file based streams because a missing header would usually indicate a corrupt or perverse file. In addition, these +APIs can take a long time to initialize because they may need to spend a lot of time finding the first frame. + + + +Build Options +============= +#define these options before including this file. + +#define DR_FLAC_NO_STDIO + Disable `drflac_open_file()` and family. + +#define DR_FLAC_NO_OGG + Disables support for Ogg/FLAC streams. + +#define DR_FLAC_BUFFER_SIZE + Defines the size of the internal buffer to store data from onRead(). This buffer is used to reduce the number of calls back to the client for more data. + Larger values means more memory, but better performance. My tests show diminishing returns after about 4KB (which is the default). Consider reducing this if + you have a very efficient implementation of onRead(), or increase it if it's very inefficient. Must be a multiple of 8. + +#define DR_FLAC_NO_CRC + Disables CRC checks. This will offer a performance boost when CRC is unnecessary. This will disable binary search seeking. When seeking, the seek table will + be used if available. Otherwise the seek will be performed using brute force. + +#define DR_FLAC_NO_SIMD + Disables SIMD optimizations (SSE on x86/x64 architectures, NEON on ARM architectures). Use this if you are having compatibility issues with your compiler. + +#define DR_FLAC_NO_WCHAR + Disables all functions ending with `_w`. Use this if your compiler does not provide wchar.h. Not required if DR_FLAC_NO_STDIO is also defined. + + + +Notes +===== +- dr_flac does not support changing the sample rate nor channel count mid stream. +- dr_flac is not thread-safe, but its APIs can be called from any thread so long as you do your own synchronization. +- When using Ogg encapsulation, a corrupted metadata block will result in `drflac_open_with_metadata()` and `drflac_open()` returning inconsistent samples due + to differences in corrupted stream recorvery logic between the two APIs. +*/ #ifndef dr_flac_h #define dr_flac_h -#include -#include -//#include +#if defined(CPPPARSER) + #define DR_FLAC_NO_SIMD +#endif -// As data is read from the client it is placed into an internal buffer for fast access. This controls the -// size of that buffer. Larger values means more speed, but also more memory. In my testing there is diminishing -// returns after about 4KB, but you can fiddle with this to suit your own needs. Must be a multiple of 8. +#ifdef __cplusplus +extern "C" { +#endif + +#define DRFLAC_STRINGIFY(x) #x +#define DRFLAC_XSTRINGIFY(x) DRFLAC_STRINGIFY(x) + +#define DRFLAC_VERSION_MAJOR 0 +#define DRFLAC_VERSION_MINOR 13 +#define DRFLAC_VERSION_REVISION 2 +#define DRFLAC_VERSION_STRING DRFLAC_XSTRINGIFY(DRFLAC_VERSION_MAJOR) "." DRFLAC_XSTRINGIFY(DRFLAC_VERSION_MINOR) "." DRFLAC_XSTRINGIFY(DRFLAC_VERSION_REVISION) + +#include /* For size_t. */ + +/* Sized Types */ +typedef signed char drflac_int8; +typedef unsigned char drflac_uint8; +typedef signed short drflac_int16; +typedef unsigned short drflac_uint16; +typedef signed int drflac_int32; +typedef unsigned int drflac_uint32; +/* Panda3D: Added !defined(CPPPARSER) so that Interrogate can parse these types correctly. */ +#if defined(_MSC_VER) && !defined(__clang__) && !defined(CPPPARSER) + typedef signed __int64 drflac_int64; + typedef unsigned __int64 drflac_uint64; +#else + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wlong-long" + #if defined(__clang__) + #pragma GCC diagnostic ignored "-Wc++11-long-long" + #endif + #endif + typedef signed long long drflac_int64; + typedef unsigned long long drflac_uint64; + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic pop + #endif +#endif +#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined(_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(__powerpc64__) + typedef drflac_uint64 drflac_uintptr; +#else + typedef drflac_uint32 drflac_uintptr; +#endif +typedef drflac_uint8 drflac_bool8; +typedef drflac_uint32 drflac_bool32; +#define DRFLAC_TRUE 1 +#define DRFLAC_FALSE 0 +/* End Sized Types */ + +/* Decorations */ +#if !defined(DRFLAC_API) + #if defined(DRFLAC_DLL) + #if defined(_WIN32) + #define DRFLAC_DLL_IMPORT __declspec(dllimport) + #define DRFLAC_DLL_EXPORT __declspec(dllexport) + #define DRFLAC_DLL_PRIVATE static + #else + #if defined(__GNUC__) && __GNUC__ >= 4 + #define DRFLAC_DLL_IMPORT __attribute__((visibility("default"))) + #define DRFLAC_DLL_EXPORT __attribute__((visibility("default"))) + #define DRFLAC_DLL_PRIVATE __attribute__((visibility("hidden"))) + #else + #define DRFLAC_DLL_IMPORT + #define DRFLAC_DLL_EXPORT + #define DRFLAC_DLL_PRIVATE static + #endif + #endif + + #if defined(DR_FLAC_IMPLEMENTATION) || defined(DRFLAC_IMPLEMENTATION) + #define DRFLAC_API DRFLAC_DLL_EXPORT + #else + #define DRFLAC_API DRFLAC_DLL_IMPORT + #endif + #define DRFLAC_PRIVATE DRFLAC_DLL_PRIVATE + #else + #define DRFLAC_API extern + #define DRFLAC_PRIVATE static + #endif +#endif +/* End Decorations */ + +#if defined(_MSC_VER) && _MSC_VER >= 1700 /* Visual Studio 2012 */ + #define DRFLAC_DEPRECATED __declspec(deprecated) +#elif (defined(__GNUC__) && __GNUC__ >= 4) /* GCC 4 */ + #define DRFLAC_DEPRECATED __attribute__((deprecated)) +#elif defined(__has_feature) /* Clang */ + #if __has_feature(attribute_deprecated) + #define DRFLAC_DEPRECATED __attribute__((deprecated)) + #else + #define DRFLAC_DEPRECATED + #endif +#else + #define DRFLAC_DEPRECATED +#endif + +DRFLAC_API void drflac_version(drflac_uint32* pMajor, drflac_uint32* pMinor, drflac_uint32* pRevision); +DRFLAC_API const char* drflac_version_string(void); + +/* Allocation Callbacks */ +typedef struct +{ + void* pUserData; + void* (* onMalloc)(size_t sz, void* pUserData); + void* (* onRealloc)(void* p, size_t sz, void* pUserData); + void (* onFree)(void* p, void* pUserData); +} drflac_allocation_callbacks; +/* End Allocation Callbacks */ + +/* +As data is read from the client it is placed into an internal buffer for fast access. This controls the size of that buffer. Larger values means more speed, +but also more memory. In my testing there is diminishing returns after about 4KB, but you can fiddle with this to suit your own needs. Must be a multiple of 8. +*/ #ifndef DR_FLAC_BUFFER_SIZE #define DR_FLAC_BUFFER_SIZE 4096 #endif -// Check if we can enable 64-bit optimizations. -#if defined(_WIN64) + +/* Architecture Detection */ +#if defined(_WIN64) || defined(_LP64) || defined(__LP64__) #define DRFLAC_64BIT #endif -#if defined(__GNUC__) -#if defined(__x86_64__) || defined(__ppc64__) -#define DRFLAC_64BIT -#endif +#if defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)) + #define DRFLAC_X64 +#elif defined(__i386) || defined(_M_IX86) + #define DRFLAC_X86 +#elif defined(__arm__) || defined(_M_ARM) || defined(__arm64) || defined(__arm64__) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) + #define DRFLAC_ARM #endif +/* End Architecture Detection */ + #ifdef DRFLAC_64BIT -typedef uint64_t drflac_cache_t; +typedef drflac_uint64 drflac_cache_t; #else -typedef uint32_t drflac_cache_t; +typedef drflac_uint32 drflac_cache_t; #endif +/* The various metadata block types. */ +#define DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO 0 +#define DRFLAC_METADATA_BLOCK_TYPE_PADDING 1 +#define DRFLAC_METADATA_BLOCK_TYPE_APPLICATION 2 +#define DRFLAC_METADATA_BLOCK_TYPE_SEEKTABLE 3 +#define DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT 4 +#define DRFLAC_METADATA_BLOCK_TYPE_CUESHEET 5 +#define DRFLAC_METADATA_BLOCK_TYPE_PICTURE 6 +#define DRFLAC_METADATA_BLOCK_TYPE_INVALID 127 +/* The various picture types specified in the PICTURE block. */ +#define DRFLAC_PICTURE_TYPE_OTHER 0 +#define DRFLAC_PICTURE_TYPE_FILE_ICON 1 +#define DRFLAC_PICTURE_TYPE_OTHER_FILE_ICON 2 +#define DRFLAC_PICTURE_TYPE_COVER_FRONT 3 +#define DRFLAC_PICTURE_TYPE_COVER_BACK 4 +#define DRFLAC_PICTURE_TYPE_LEAFLET_PAGE 5 +#define DRFLAC_PICTURE_TYPE_MEDIA 6 +#define DRFLAC_PICTURE_TYPE_LEAD_ARTIST 7 +#define DRFLAC_PICTURE_TYPE_ARTIST 8 +#define DRFLAC_PICTURE_TYPE_CONDUCTOR 9 +#define DRFLAC_PICTURE_TYPE_BAND 10 +#define DRFLAC_PICTURE_TYPE_COMPOSER 11 +#define DRFLAC_PICTURE_TYPE_LYRICIST 12 +#define DRFLAC_PICTURE_TYPE_RECORDING_LOCATION 13 +#define DRFLAC_PICTURE_TYPE_DURING_RECORDING 14 +#define DRFLAC_PICTURE_TYPE_DURING_PERFORMANCE 15 +#define DRFLAC_PICTURE_TYPE_SCREEN_CAPTURE 16 +#define DRFLAC_PICTURE_TYPE_BRIGHT_COLORED_FISH 17 +#define DRFLAC_PICTURE_TYPE_ILLUSTRATION 18 +#define DRFLAC_PICTURE_TYPE_BAND_LOGOTYPE 19 +#define DRFLAC_PICTURE_TYPE_PUBLISHER_LOGOTYPE 20 -// Callback for when data is read. Return value is the number of bytes actually read. -typedef size_t (* drflac_read_proc)(void* userData, void* bufferOut, size_t bytesToRead); +typedef enum +{ + drflac_container_native, + drflac_container_ogg, + drflac_container_unknown +} drflac_container; -// Callback for when data needs to be seeked. Offset is always relative to the current position. Return value is false on failure, true success. -typedef bool (* drflac_seek_proc)(void* userData, int offset); +typedef enum +{ + DRFLAC_SEEK_SET, + DRFLAC_SEEK_CUR, + DRFLAC_SEEK_END +} drflac_seek_origin; +/* The order of members in this structure is important because we map this directly to the raw data within the SEEKTABLE metadata block. */ +typedef struct +{ + drflac_uint64 firstPCMFrame; + drflac_uint64 flacFrameOffset; /* The offset from the first byte of the header of the first frame. */ + drflac_uint16 pcmFrameCount; +} drflac_seekpoint; typedef struct { - // The absolute position of the first byte of the data of the block. This is just past the block's header. - long long pos; - - // The size in bytes of the block's data. - unsigned int sizeInBytes; - -} drflac_block; + drflac_uint16 minBlockSizeInPCMFrames; + drflac_uint16 maxBlockSizeInPCMFrames; + drflac_uint32 minFrameSizeInPCMFrames; + drflac_uint32 maxFrameSizeInPCMFrames; + drflac_uint32 sampleRate; + drflac_uint8 channels; + drflac_uint8 bitsPerSample; + drflac_uint64 totalPCMFrameCount; + drflac_uint8 md5[16]; +} drflac_streaminfo; typedef struct { - // The type of the subframe: SUBFRAME_CONSTANT, SUBFRAME_VERBATIM, SUBFRAME_FIXED or SUBFRAME_LPC. - unsigned char subframeType; + /* + The metadata type. Use this to know how to interpret the data below. Will be set to one of the + DRFLAC_METADATA_BLOCK_TYPE_* tokens. + */ + drflac_uint32 type; - // The number of wasted bits per sample as specified by the sub-frame header. - unsigned char wastedBitsPerSample; + /* The size in bytes of the block and the buffer pointed to by pRawData if it's non-NULL. */ + drflac_uint32 rawDataSize; - // The order to use for the prediction stage for SUBFRAME_FIXED and SUBFRAME_LPC. - unsigned char lpcOrder; + /* The offset in the stream of the raw data. */ + drflac_uint64 rawDataOffset; - // The number of bits per sample for this subframe. This is not always equal to the current frame's bit per sample because - // an extra bit is required for side channels when interchannel decorrelation is being used. - int bitsPerSample; + /* + A pointer to the raw data. This points to a temporary buffer so don't hold on to it. It's best to + not modify the contents of this buffer. Use the structures below for more meaningful and structured + information about the metadata. It's possible for this to be null. + */ + const void* pRawData; - // A pointer to the buffer containing the decoded samples in the subframe. This pointer is an offset from drflac::pHeap, or - // NULL if the heap is not being used. Note that it's a signed 32-bit integer for each value. - int32_t* pDecodedSamples; + union + { + drflac_streaminfo streaminfo; + struct + { + int unused; + } padding; + + struct + { + drflac_uint32 id; + const void* pData; + drflac_uint32 dataSize; + } application; + + struct + { + drflac_uint32 seekpointCount; + const drflac_seekpoint* pSeekpoints; + } seektable; + + struct + { + drflac_uint32 vendorLength; + const char* vendor; + drflac_uint32 commentCount; + const void* pComments; + } vorbis_comment; + + struct + { + char catalog[128]; + drflac_uint64 leadInSampleCount; + drflac_bool32 isCD; + drflac_uint8 trackCount; + const void* pTrackData; + } cuesheet; + + struct + { + drflac_uint32 type; + drflac_uint32 mimeLength; + const char* mime; + drflac_uint32 descriptionLength; + const char* description; + drflac_uint32 width; + drflac_uint32 height; + drflac_uint32 colorDepth; + drflac_uint32 indexColorCount; + drflac_uint32 pictureDataSize; + drflac_uint64 pictureDataOffset; /* Offset from the start of the stream. */ + const drflac_uint8* pPictureData; + } picture; + } data; +} drflac_metadata; + + +/* +Callback for when data needs to be read from the client. + + +Parameters +---------- +pUserData (in) + The user data that was passed to drflac_open() and family. + +pBufferOut (out) + The output buffer. + +bytesToRead (in) + The number of bytes to read. + + +Return Value +------------ +The number of bytes actually read. + + +Remarks +------- +A return value of less than bytesToRead indicates the end of the stream. Do _not_ return from this callback until either the entire bytesToRead is filled or +you have reached the end of the stream. +*/ +typedef size_t (* drflac_read_proc)(void* pUserData, void* pBufferOut, size_t bytesToRead); + +/* +Callback for when data needs to be seeked. + + +Parameters +---------- +pUserData (in) + The user data that was passed to drflac_open() and family. + +offset (in) + The number of bytes to move, relative to the origin. Will never be negative. + +origin (in) + The origin of the seek - the current position, the start of the stream, or the end of the stream. + + +Return Value +------------ +Whether or not the seek was successful. + + +Remarks +------- +Seeking relative to the start and the current position must always be supported. If seeking from the end of the stream is not supported, return DRFLAC_FALSE. + +When seeking to a PCM frame using drflac_seek_to_pcm_frame(), dr_flac may call this with an offset beyond the end of the FLAC stream. This needs to be detected +and handled by returning DRFLAC_FALSE. +*/ +typedef drflac_bool32 (* drflac_seek_proc)(void* pUserData, int offset, drflac_seek_origin origin); + +/* +Callback for when the current position in the stream needs to be retrieved. + + +Parameters +---------- +pUserData (in) + The user data that was passed to drflac_open() and family. + +pCursor (out) + A pointer to a variable to receive the current position in the stream. + + +Return Value +------------ +Whether or not the operation was successful. +*/ +typedef drflac_bool32 (* drflac_tell_proc)(void* pUserData, drflac_int64* pCursor); + +/* +Callback for when a metadata block is read. + + +Parameters +---------- +pUserData (in) + The user data that was passed to drflac_open() and family. + +pMetadata (in) + A pointer to a structure containing the data of the metadata block. + + +Remarks +------- +Use pMetadata->type to determine which metadata block is being handled and how to read the data. This +will be set to one of the DRFLAC_METADATA_BLOCK_TYPE_* tokens. +*/ +typedef void (* drflac_meta_proc)(void* pUserData, drflac_metadata* pMetadata); + + +/* Structure for internal use. Only used for decoders opened with drflac_open_memory. */ +typedef struct +{ + const drflac_uint8* data; + size_t dataSize; + size_t currentReadPos; +} drflac__memory_stream; + +/* Structure for internal use. Used for bit streaming. */ +typedef struct +{ + /* The function to call when more data needs to be read. */ + drflac_read_proc onRead; + + /* The function to call when the current read position needs to be moved. */ + drflac_seek_proc onSeek; + + /* The function to call when the current read position needs to be retrieved. */ + drflac_tell_proc onTell; + + /* The user data to pass around to onRead and onSeek. */ + void* pUserData; + + + /* + The number of unaligned bytes in the L2 cache. This will always be 0 until the end of the stream is hit. At the end of the + stream there will be a number of bytes that don't cleanly fit in an L1 cache line, so we use this variable to know whether + or not the bistreamer needs to run on a slower path to read those last bytes. This will never be more than sizeof(drflac_cache_t). + */ + size_t unalignedByteCount; + + /* The content of the unaligned bytes. */ + drflac_cache_t unalignedCache; + + /* The index of the next valid cache line in the "L2" cache. */ + drflac_uint32 nextL2Line; + + /* The number of bits that have been consumed by the cache. This is used to determine how many valid bits are remaining. */ + drflac_uint32 consumedBits; + + /* + The cached data which was most recently read from the client. There are two levels of cache. Data flows as such: + Client -> L2 -> L1. The L2 -> L1 movement is aligned and runs on a fast path in just a few instructions. + */ + drflac_cache_t cacheL2[DR_FLAC_BUFFER_SIZE/sizeof(drflac_cache_t)]; + drflac_cache_t cache; + + /* + CRC-16. This is updated whenever bits are read from the bit stream. Manually set this to 0 to reset the CRC. For FLAC, this + is reset to 0 at the beginning of each frame. + */ + drflac_uint16 crc16; + drflac_cache_t crc16Cache; /* A cache for optimizing CRC calculations. This is filled when when the L1 cache is reloaded. */ + drflac_uint32 crc16CacheIgnoredBytes; /* The number of bytes to ignore when updating the CRC-16 from the CRC-16 cache. */ +} drflac_bs; + +typedef struct +{ + /* The type of the subframe: SUBFRAME_CONSTANT, SUBFRAME_VERBATIM, SUBFRAME_FIXED or SUBFRAME_LPC. */ + drflac_uint8 subframeType; + + /* The number of wasted bits per sample as specified by the sub-frame header. */ + drflac_uint8 wastedBitsPerSample; + + /* The order to use for the prediction stage for SUBFRAME_FIXED and SUBFRAME_LPC. */ + drflac_uint8 lpcOrder; + + /* A pointer to the buffer containing the decoded samples in the subframe. This pointer is an offset from drflac::pExtraData. */ + drflac_int32* pSamplesS32; } drflac_subframe; typedef struct { - // If the stream uses variable block sizes, this will be set to the index of the first sample. If fixed block sizes are used, this will - // always be set to 0. - unsigned long long sampleNumber; + /* + If the stream uses variable block sizes, this will be set to the index of the first PCM frame. If fixed block sizes are used, this will + always be set to 0. This is 64-bit because the decoded PCM frame number will be 36 bits. + */ + drflac_uint64 pcmFrameNumber; - // If the stream uses fixed block sizes, this will be set to the frame number. If variable block sizes are used, this will always be 0. - unsigned int frameNumber; + /* + If the stream uses fixed block sizes, this will be set to the frame number. If variable block sizes are used, this will always be 0. This + is 32-bit because in fixed block sizes, the maximum frame number will be 31 bits. + */ + drflac_uint32 flacFrameNumber; - // The sample rate of this frame. - unsigned int sampleRate; + /* The sample rate of this frame. */ + drflac_uint32 sampleRate; - // The number of samples in each sub-frame within this frame. - unsigned short blockSize; + /* The number of PCM frames in each sub-frame within this frame. */ + drflac_uint16 blockSizeInPCMFrames; - // The channel assignment of this frame. This is not always set to the channel count. If interchannel decorrelation is being used this - // will be set to DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE, DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE or DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE. - unsigned char channelAssignment; + /* + The channel assignment of this frame. This is not always set to the channel count. If interchannel decorrelation is being used this + will be set to DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE, DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE or DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE. + */ + drflac_uint8 channelAssignment; - // The number of bits per sample within this frame. - unsigned char bitsPerSample; + /* The number of bits per sample within this frame. */ + drflac_uint8 bitsPerSample; - // The frame's CRC. This is set, but unused at the moment. - unsigned char crc8; + /* The frame's CRC. */ + drflac_uint8 crc8; +} drflac_frame_header; - // The number of samples left to be read in this frame. This is initially set to the block size multiplied by the channel count. As samples - // are read, this will be decremented. When it reaches 0, the decoder will see this frame as fully consumed and load the next frame. - unsigned int samplesRemaining; +typedef struct +{ + /* The header. */ + drflac_frame_header header; - // The list of sub-frames within the frame. There is one sub-frame for each channel, and there's a maximum of 8 channels. + /* + The number of PCM frames left to be read in this FLAC frame. This is initially set to the block size. As PCM frames are read, + this will be decremented. When it reaches 0, the decoder will see this frame as fully consumed and load the next frame. + */ + drflac_uint32 pcmFramesRemaining; + + /* The list of sub-frames within the frame. There is one sub-frame for each channel, and there's a maximum of 8 channels. */ drflac_subframe subframes[8]; - } drflac_frame; typedef struct { - // The function to call when more data needs to be read. This is set by drflac_open(). - drflac_read_proc onRead; + /* The function to call when a metadata block is read. */ + drflac_meta_proc onMeta; - // The function to call when the current read position needs to be moved. - drflac_seek_proc onSeek; + /* The user data posted to the metadata callback function. */ + void* pUserDataMD; - // The user data to pass around to onRead and onSeek. - void* pUserData; + /* Memory allocation callbacks. */ + drflac_allocation_callbacks allocationCallbacks; - // The sample rate. Will be set to something like 44100. - unsigned int sampleRate; + /* The sample rate. Will be set to something like 44100. */ + drflac_uint32 sampleRate; - // The number of channels. This will be set to 1 for monaural streams, 2 for stereo, etc. Maximum 8. This is set based on the - // value specified in the STREAMINFO block. - unsigned char channels; + /* + The number of channels. This will be set to 1 for monaural streams, 2 for stereo, etc. Maximum 8. This is set based on the + value specified in the STREAMINFO block. + */ + drflac_uint8 channels; - // The bits per sample. Will be set to somthing like 16, 24, etc. - unsigned char bitsPerSample; + /* The bits per sample. Will be set to something like 16, 24, etc. */ + drflac_uint8 bitsPerSample; - // The maximum block size, in samples. This number represents the number of samples in each channel (not combined). - unsigned short maxBlockSize; + /* The maximum block size, in samples. This number represents the number of samples in each channel (not combined). */ + drflac_uint16 maxBlockSizeInPCMFrames; - // The total number of samples making up the stream. This includes every channel. For example, if the stream has 2 channels, - // with each channel having a total of 4096, this value will be set to 2*4096 = 8192. - uint64_t totalSampleCount; + /* + The total number of PCM Frames making up the stream. Can be 0 in which case it's still a valid stream, but just means + the total PCM frame count is unknown. Likely the case with streams like internet radio. + */ + drflac_uint64 totalPCMFrameCount; - // The location and size of the APPLICATION block. - drflac_block applicationBlock; + /* The container type. This is set based on whether or not the decoder was opened from a native or Ogg stream. */ + drflac_container container; - // The location and size of the SEEKTABLE block. - drflac_block seektableBlock; - - // The location and size of the VORBIS_COMMENT block. - drflac_block vorbisCommentBlock; - - // The location and size of the CUESHEET block. - drflac_block cuesheetBlock; - - // The location and size of the PICTURE block. - drflac_block pictureBlock; + /* The number of seekpoints in the seektable. */ + drflac_uint32 seekpointCount; - // Information about the frame the decoder is currently sitting on. - drflac_frame currentFrame; - - // The position of the first frame in the stream. This is only ever used for seeking. - unsigned long long firstFramePos; + /* Information about the frame the decoder is currently sitting on. */ + drflac_frame currentFLACFrame; + /* The index of the PCM frame the decoder is currently sitting on. This is only used for seeking. */ + drflac_uint64 currentPCMFrame; - // The current byte position in the client's data stream. - uint64_t currentBytePos; - - // The index of the next valid cache line in the "L2" cache. - size_t nextL2Line; - - // The number of bits that have been consumed by the cache. This is used to determine how many valid bits are remaining. - size_t consumedBits; - - // Unused L2 lines. This will always be 0 until the end of the stream is hit. Used for correctly calculating the current byte - // position of the read pointer in the stream. - size_t unusedL2Lines; - - // The cached data which was most recently read from the client. When data is read from the client, it is placed within this - // variable. As data is read, it's bit-shifted such that the next valid bit is sitting on the most significant bit. - drflac_cache_t cache; - drflac_cache_t cacheL2[DR_FLAC_BUFFER_SIZE/sizeof(drflac_cache_t)]; + /* The position of the first FLAC frame in the stream. This is only ever used for seeking. */ + drflac_uint64 firstFLACFramePosInBytes; - // A pointer to the decoded sample data. This is an offset of pExtraData. - int32_t* pDecodedSamples; + /* A hack to avoid a malloc() when opening a decoder with drflac_open_memory(). */ + drflac__memory_stream memoryStream; - // Variable length extra data. We attach this to the end of the object so we avoid unnecessary mallocs. - char pExtraData[1]; + /* A pointer to the decoded sample data. This is an offset of pExtraData. */ + drflac_int32* pDecodedSamples; + + /* A pointer to the seek table. This is an offset of pExtraData, or NULL if there is no seek table. */ + drflac_seekpoint* pSeekpoints; + + /* Internal use only. Only used with Ogg containers. Points to a drflac_oggbs object. This is an offset of pExtraData. */ + void* _oggbs; + + /* Internal use only. Used for profiling and testing different seeking modes. */ + drflac_bool32 _noSeekTableSeek : 1; + drflac_bool32 _noBinarySearchSeek : 1; + drflac_bool32 _noBruteForceSeek : 1; + + /* The bit streamer. The raw FLAC data is fed through this object. */ + drflac_bs bs; + + /* Variable length extra data. We attach this to the end of the object so we can avoid unnecessary mallocs. */ + drflac_uint8 pExtraData[1]; } drflac; +/* +Opens a FLAC decoder. -// Opens a FLAC decoder. -// -// This is the lowest level function for opening a FLAC stream. You can also use drflac_open_file() and drflac_open_memory() -// to open the stream from a file or from a block of memory respectively. -// -// At the moment the STREAMINFO block must be present for this to succeed. -// -// The onRead and onSeek callbacks are used to read and seek data provided by the client. -static drflac* drflac_open(drflac_read_proc onRead, drflac_seek_proc onSeek, void* pUserData); +Parameters +---------- +onRead (in) + The function to call when data needs to be read from the client. -// Closes the given FLAC decoder. -static void drflac_close(drflac* pFlac); +onSeek (in) + The function to call when the read position of the client data needs to move. -// Reads sample data from the given FLAC decoder, output as interleaved signed 16-bit PCM. -// -// Returns the number of samples actually read. -static uint64_t drflac_read_s16(drflac* pFlac, uint64_t samplesToRead, int16_t* pBufferOut); +pUserData (in, optional) + A pointer to application defined data that will be passed to onRead and onSeek. -// Seeks to the sample at the given index. -static bool drflac_seek_to_sample(drflac* pFlac, uint64_t sampleIndex); +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +Returns a pointer to an object representing the decoder. + + +Remarks +------- +Close the decoder with `drflac_close()`. + +`pAllocationCallbacks` can be NULL in which case it will use `DRFLAC_MALLOC`, `DRFLAC_REALLOC` and `DRFLAC_FREE`. + +This function will automatically detect whether or not you are attempting to open a native or Ogg encapsulated FLAC, both of which should work seamlessly +without any manual intervention. Ogg encapsulation also works with multiplexed streams which basically means it can play FLAC encoded audio tracks in videos. + +This is the lowest level function for opening a FLAC stream. You can also use `drflac_open_file()` and `drflac_open_memory()` to open the stream from a file or +from a block of memory respectively. + +The STREAMINFO block must be present for this to succeed. Use `drflac_open_relaxed()` to open a FLAC stream where the header may not be present. + +Use `drflac_open_with_metadata()` if you need access to metadata. + + +Seek Also +--------- +drflac_open_file() +drflac_open_memory() +drflac_open_with_metadata() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Opens a FLAC stream with relaxed validation of the header block. + + +Parameters +---------- +onRead (in) + The function to call when data needs to be read from the client. + +onSeek (in) + The function to call when the read position of the client data needs to move. + +container (in) + Whether or not the FLAC stream is encapsulated using standard FLAC encapsulation or Ogg encapsulation. + +pUserData (in, optional) + A pointer to application defined data that will be passed to onRead and onSeek. + +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +A pointer to an object representing the decoder. + + +Remarks +------- +The same as drflac_open(), except attempts to open the stream even when a header block is not present. + +Because the header is not necessarily available, the caller must explicitly define the container (Native or Ogg). Do not set this to `drflac_container_unknown` +as that is for internal use only. + +Opening in relaxed mode will continue reading data from onRead until it finds a valid frame. If a frame is never found it will continue forever. To abort, +force your `onRead` callback to return 0, which dr_flac will use as an indicator that the end of the stream was found. + +Use `drflac_open_with_metadata_relaxed()` if you need access to metadata. +*/ +DRFLAC_API drflac* drflac_open_relaxed(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_container container, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Opens a FLAC decoder and notifies the caller of the metadata chunks (album art, etc.). + + +Parameters +---------- +onRead (in) + The function to call when data needs to be read from the client. + +onSeek (in) + The function to call when the read position of the client data needs to move. + +onMeta (in) + The function to call for every metadata block. + +pUserData (in, optional) + A pointer to application defined data that will be passed to onRead, onSeek and onMeta. + +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +A pointer to an object representing the decoder. + + +Remarks +------- +Close the decoder with `drflac_close()`. + +`pAllocationCallbacks` can be NULL in which case it will use `DRFLAC_MALLOC`, `DRFLAC_REALLOC` and `DRFLAC_FREE`. + +This is slower than `drflac_open()`, so avoid this one if you don't need metadata. Internally, this will allocate and free memory on the heap for every +metadata block except for STREAMINFO and PADDING blocks. + +The caller is notified of the metadata via the `onMeta` callback. All metadata blocks will be handled before the function returns. This callback takes a +pointer to a `drflac_metadata` object which is a union containing the data of all relevant metadata blocks. Use the `type` member to discriminate against +the different metadata types. + +The STREAMINFO block must be present for this to succeed. Use `drflac_open_with_metadata_relaxed()` to open a FLAC stream where the header may not be present. + +Note that this will behave inconsistently with `drflac_open()` if the stream is an Ogg encapsulated stream and a metadata block is corrupted. This is due to +the way the Ogg stream recovers from corrupted pages. When `drflac_open_with_metadata()` is being used, the open routine will try to read the contents of the +metadata block, whereas `drflac_open()` will simply seek past it (for the sake of efficiency). This inconsistency can result in different samples being +returned depending on whether or not the stream is being opened with metadata. + + +Seek Also +--------- +drflac_open_file_with_metadata() +drflac_open_memory_with_metadata() +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_with_metadata(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +The same as drflac_open_with_metadata(), except attempts to open the stream even when a header block is not present. + +See Also +-------- +drflac_open_with_metadata() +drflac_open_relaxed() +*/ +DRFLAC_API drflac* drflac_open_with_metadata_relaxed(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, drflac_container container, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Closes the given FLAC decoder. + + +Parameters +---------- +pFlac (in) + The decoder to close. + + +Remarks +------- +This will destroy the decoder object. + + +See Also +-------- +drflac_open() +drflac_open_with_metadata() +drflac_open_file() +drflac_open_file_w() +drflac_open_file_with_metadata() +drflac_open_file_with_metadata_w() +drflac_open_memory() +drflac_open_memory_with_metadata() +*/ +DRFLAC_API void drflac_close(drflac* pFlac); + + +/* +Reads sample data from the given FLAC decoder, output as interleaved signed 32-bit PCM. + + +Parameters +---------- +pFlac (in) + The decoder. + +framesToRead (in) + The number of PCM frames to read. + +pBufferOut (out, optional) + A pointer to the buffer that will receive the decoded samples. + + +Return Value +------------ +Returns the number of PCM frames actually read. If the return value is less than `framesToRead` it has reached the end. + + +Remarks +------- +pBufferOut can be null, in which case the call will act as a seek, and the return value will be the number of frames seeked. +*/ +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_s32(drflac* pFlac, drflac_uint64 framesToRead, drflac_int32* pBufferOut); + + +/* +Reads sample data from the given FLAC decoder, output as interleaved signed 16-bit PCM. + + +Parameters +---------- +pFlac (in) + The decoder. + +framesToRead (in) + The number of PCM frames to read. + +pBufferOut (out, optional) + A pointer to the buffer that will receive the decoded samples. + + +Return Value +------------ +Returns the number of PCM frames actually read. If the return value is less than `framesToRead` it has reached the end. + + +Remarks +------- +pBufferOut can be null, in which case the call will act as a seek, and the return value will be the number of frames seeked. + +Note that this is lossy for streams where the bits per sample is larger than 16. +*/ +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_s16(drflac* pFlac, drflac_uint64 framesToRead, drflac_int16* pBufferOut); + +/* +Reads sample data from the given FLAC decoder, output as interleaved 32-bit floating point PCM. + + +Parameters +---------- +pFlac (in) + The decoder. + +framesToRead (in) + The number of PCM frames to read. + +pBufferOut (out, optional) + A pointer to the buffer that will receive the decoded samples. + + +Return Value +------------ +Returns the number of PCM frames actually read. If the return value is less than `framesToRead` it has reached the end. + + +Remarks +------- +pBufferOut can be null, in which case the call will act as a seek, and the return value will be the number of frames seeked. + +Note that this should be considered lossy due to the nature of floating point numbers not being able to exactly represent every possible number. +*/ +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_f32(drflac* pFlac, drflac_uint64 framesToRead, float* pBufferOut); + +/* +Seeks to the PCM frame at the given index. + + +Parameters +---------- +pFlac (in) + The decoder. + +pcmFrameIndex (in) + The index of the PCM frame to seek to. See notes below. + + +Return Value +------------- +`DRFLAC_TRUE` if successful; `DRFLAC_FALSE` otherwise. +*/ +DRFLAC_API drflac_bool32 drflac_seek_to_pcm_frame(drflac* pFlac, drflac_uint64 pcmFrameIndex); #ifndef DR_FLAC_NO_STDIO -// Opens a flac decoder from the file at the given path. -static drflac* drflac_open_file(const char* pFile); +/* +Opens a FLAC decoder from the file at the given path. + + +Parameters +---------- +pFileName (in) + The path of the file to open, either absolute or relative to the current directory. + +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +A pointer to an object representing the decoder. + + +Remarks +------- +Close the decoder with drflac_close(). + + +Remarks +------- +This will hold a handle to the file until the decoder is closed with drflac_close(). Some platforms will restrict the number of files a process can have open +at any given time, so keep this mind if you have many decoders open at the same time. + + +See Also +-------- +drflac_open_file_with_metadata() +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_file(const char* pFileName, const drflac_allocation_callbacks* pAllocationCallbacks); +DRFLAC_API drflac* drflac_open_file_w(const wchar_t* pFileName, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Opens a FLAC decoder from the file at the given path and notifies the caller of the metadata chunks (album art, etc.) + + +Parameters +---------- +pFileName (in) + The path of the file to open, either absolute or relative to the current directory. + +pAllocationCallbacks (in, optional) + A pointer to application defined callbacks for managing memory allocations. + +onMeta (in) + The callback to fire for each metadata block. + +pUserData (in) + A pointer to the user data to pass to the metadata callback. + +pAllocationCallbacks (in) + A pointer to application defined callbacks for managing memory allocations. + + +Remarks +------- +Look at the documentation for drflac_open_with_metadata() for more information on how metadata is handled. + + +See Also +-------- +drflac_open_with_metadata() +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_file_with_metadata(const char* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); +DRFLAC_API drflac* drflac_open_file_with_metadata_w(const wchar_t* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); #endif -// Helper for opening a file from a pre-allocated memory buffer. -// -// This does not create a copy of the data. It is up to the application to ensure the buffer remains valid for -// the lifetime of the decoder. -static drflac* drflac_open_memory(const void* data, size_t dataSize); - -#endif //dr_flac_h +/* +Opens a FLAC decoder from a pre-allocated block of memory -/////////////////////////////////////////////////////////////////////////////// -// -// IMPLEMENTATION -// -/////////////////////////////////////////////////////////////////////////////// -#ifdef DR_FLAC_IMPLEMENTATION -#include -#include -#include +Parameters +---------- +pData (in) + A pointer to the raw encoded FLAC data. -#ifdef _MSC_VER -#include // For _byteswap_ulong and _byteswap_uint64 +dataSize (in) + The size in bytes of `data`. + +pAllocationCallbacks (in) + A pointer to application defined callbacks for managing memory allocations. + + +Return Value +------------ +A pointer to an object representing the decoder. + + +Remarks +------- +This does not create a copy of the data. It is up to the application to ensure the buffer remains valid for the lifetime of the decoder. + + +See Also +-------- +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_memory(const void* pData, size_t dataSize, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Opens a FLAC decoder from a pre-allocated block of memory and notifies the caller of the metadata chunks (album art, etc.) + + +Parameters +---------- +pData (in) + A pointer to the raw encoded FLAC data. + +dataSize (in) + The size in bytes of `data`. + +onMeta (in) + The callback to fire for each metadata block. + +pUserData (in) + A pointer to the user data to pass to the metadata callback. + +pAllocationCallbacks (in) + A pointer to application defined callbacks for managing memory allocations. + + +Remarks +------- +Look at the documentation for drflac_open_with_metadata() for more information on how metadata is handled. + + +See Also +------- +drflac_open_with_metadata() +drflac_open() +drflac_close() +*/ +DRFLAC_API drflac* drflac_open_memory_with_metadata(const void* pData, size_t dataSize, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks); + + + +/* High Level APIs */ + +/* +Opens a FLAC stream from the given callbacks and fully decodes it in a single operation. The return value is a +pointer to the sample data as interleaved signed 32-bit PCM. The returned data must be freed with drflac_free(). + +You can pass in custom memory allocation callbacks via the pAllocationCallbacks parameter. This can be NULL in which +case it will use DRFLAC_MALLOC, DRFLAC_REALLOC and DRFLAC_FREE. + +Sometimes a FLAC file won't keep track of the total sample count. In this situation the function will continuously +read samples into a dynamically sized buffer on the heap until no samples are left. + +Do not call this function on a broadcast type of stream (like internet radio streams and whatnot). +*/ +DRFLAC_API drflac_int32* drflac_open_and_read_pcm_frames_s32(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_and_read_pcm_frames_s32(), except returns signed 16-bit integer samples. */ +DRFLAC_API drflac_int16* drflac_open_and_read_pcm_frames_s16(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_and_read_pcm_frames_s32(), except returns 32-bit floating-point samples. */ +DRFLAC_API float* drflac_open_and_read_pcm_frames_f32(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +#ifndef DR_FLAC_NO_STDIO +/* Same as drflac_open_and_read_pcm_frames_s32() except opens the decoder from a file. */ +DRFLAC_API drflac_int32* drflac_open_file_and_read_pcm_frames_s32(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_file_and_read_pcm_frames_s32(), except returns signed 16-bit integer samples. */ +DRFLAC_API drflac_int16* drflac_open_file_and_read_pcm_frames_s16(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_file_and_read_pcm_frames_s32(), except returns 32-bit floating-point samples. */ +DRFLAC_API float* drflac_open_file_and_read_pcm_frames_f32(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); +#endif + +/* Same as drflac_open_and_read_pcm_frames_s32() except opens the decoder from a block of memory. */ +DRFLAC_API drflac_int32* drflac_open_memory_and_read_pcm_frames_s32(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_memory_and_read_pcm_frames_s32(), except returns signed 16-bit integer samples. */ +DRFLAC_API drflac_int16* drflac_open_memory_and_read_pcm_frames_s16(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* Same as drflac_open_memory_and_read_pcm_frames_s32(), except returns 32-bit floating-point samples. */ +DRFLAC_API float* drflac_open_memory_and_read_pcm_frames_f32(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks); + +/* +Frees memory that was allocated internally by dr_flac. + +Set pAllocationCallbacks to the same object that was passed to drflac_open_*_and_read_pcm_frames_*(). If you originally passed in NULL, pass in NULL for this. +*/ +DRFLAC_API void drflac_free(void* p, const drflac_allocation_callbacks* pAllocationCallbacks); + + +/* Structure representing an iterator for vorbis comments in a VORBIS_COMMENT metadata block. */ +typedef struct +{ + drflac_uint32 countRemaining; + const char* pRunningData; +} drflac_vorbis_comment_iterator; + +/* +Initializes a vorbis comment iterator. This can be used for iterating over the vorbis comments in a VORBIS_COMMENT +metadata block. +*/ +DRFLAC_API void drflac_init_vorbis_comment_iterator(drflac_vorbis_comment_iterator* pIter, drflac_uint32 commentCount, const void* pComments); + +/* +Goes to the next vorbis comment in the given iterator. If null is returned it means there are no more comments. The +returned string is NOT null terminated. +*/ +DRFLAC_API const char* drflac_next_vorbis_comment(drflac_vorbis_comment_iterator* pIter, drflac_uint32* pCommentLengthOut); + + +/* Structure representing an iterator for cuesheet tracks in a CUESHEET metadata block. */ +typedef struct +{ + drflac_uint32 countRemaining; + const char* pRunningData; +} drflac_cuesheet_track_iterator; + +/* The order of members here is important because we map this directly to the raw data within the CUESHEET metadata block. */ +typedef struct +{ + drflac_uint64 offset; + drflac_uint8 index; + drflac_uint8 reserved[3]; +} drflac_cuesheet_track_index; + +typedef struct +{ + drflac_uint64 offset; + drflac_uint8 trackNumber; + char ISRC[12]; + drflac_bool8 isAudio; + drflac_bool8 preEmphasis; + drflac_uint8 indexCount; + const drflac_cuesheet_track_index* pIndexPoints; +} drflac_cuesheet_track; + +/* +Initializes a cuesheet track iterator. This can be used for iterating over the cuesheet tracks in a CUESHEET metadata +block. +*/ +DRFLAC_API void drflac_init_cuesheet_track_iterator(drflac_cuesheet_track_iterator* pIter, drflac_uint32 trackCount, const void* pTrackData); + +/* Goes to the next cuesheet track in the given iterator. If DRFLAC_FALSE is returned it means there are no more comments. */ +DRFLAC_API drflac_bool32 drflac_next_cuesheet_track(drflac_cuesheet_track_iterator* pIter, drflac_cuesheet_track* pCuesheetTrack); + + +#ifdef __cplusplus +} +#endif +#endif /* dr_flac_h */ + + +/************************************************************************************************************************************************************ + ************************************************************************************************************************************************************ + + IMPLEMENTATION + + ************************************************************************************************************************************************************ + ************************************************************************************************************************************************************/ +#if defined(DR_FLAC_IMPLEMENTATION) || defined(DRFLAC_IMPLEMENTATION) +#ifndef dr_flac_c +#define dr_flac_c + +/* Disable some annoying warnings. */ +#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic push + #if __GNUC__ >= 7 + #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" + #endif #endif #ifdef __linux__ -#ifndef _BSD_SOURCE -#define _BSD_SOURCE -#endif -#include + #ifndef _BSD_SOURCE + #define _BSD_SOURCE + #endif + #ifndef _DEFAULT_SOURCE + #define _DEFAULT_SOURCE + #endif + #ifndef __USE_BSD + #define __USE_BSD + #endif + #include #endif -#define DRFLAC_INLINE ALWAYS_INLINE +#include +#include + +/* Inline */ +#ifdef _MSC_VER + #if defined(CPPPARSER) + #define DRFLAC_INLINE inline + #else + #define DRFLAC_INLINE __forceinline + #endif +#elif defined(__GNUC__) + /* + I've had a bug report where GCC is emitting warnings about functions possibly not being inlineable. This warning happens when + the __attribute__((always_inline)) attribute is defined without an "inline" statement. I think therefore there must be some + case where "__inline__" is not always defined, thus the compiler emitting these warnings. When using -std=c89 or -ansi on the + command line, we cannot use the "inline" keyword and instead need to use "__inline__". In an attempt to work around this issue + I am using "__inline__" only when we're compiling in strict ANSI mode. + */ + #if defined(__STRICT_ANSI__) + #define DRFLAC_GNUC_INLINE_HINT __inline__ + #else + #define DRFLAC_GNUC_INLINE_HINT inline + #endif + + #if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 2)) || defined(__clang__) + #define DRFLAC_INLINE DRFLAC_GNUC_INLINE_HINT __attribute__((always_inline)) + #else + #define DRFLAC_INLINE DRFLAC_GNUC_INLINE_HINT + #endif +#elif defined(__WATCOMC__) + #define DRFLAC_INLINE __inline +#else + #define DRFLAC_INLINE +#endif +/* End Inline */ + +/* +Intrinsics Support + +There's a bug in GCC 4.2.x which results in an incorrect compilation error when using _mm_slli_epi32() where it complains with + + "error: shift must be an immediate" + +Unfortuantely dr_flac depends on this for a few things so we're just going to disable SSE on GCC 4.2 and below. +*/ +#if !defined(DR_FLAC_NO_SIMD) + #if defined(DRFLAC_X64) || defined(DRFLAC_X86) + #if defined(_MSC_VER) && !defined(__clang__) + /* MSVC. */ + #if _MSC_VER >= 1400 && !defined(DRFLAC_NO_SSE2) /* 2005 */ + #define DRFLAC_SUPPORT_SSE2 + #endif + #if _MSC_VER >= 1600 && !defined(DRFLAC_NO_SSE41) /* 2010 */ + #define DRFLAC_SUPPORT_SSE41 + #endif + #elif defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3))) + /* Assume GNUC-style. */ + #if defined(__SSE2__) && !defined(DRFLAC_NO_SSE2) + #define DRFLAC_SUPPORT_SSE2 + #endif + #if defined(__SSE4_1__) && !defined(DRFLAC_NO_SSE41) + #define DRFLAC_SUPPORT_SSE41 + #endif + #endif + + /* If at this point we still haven't determined compiler support for the intrinsics just fall back to __has_include. */ + #if !defined(__GNUC__) && !defined(__clang__) && defined(__has_include) + #if !defined(DRFLAC_SUPPORT_SSE2) && !defined(DRFLAC_NO_SSE2) && __has_include() + #define DRFLAC_SUPPORT_SSE2 + #endif + #if !defined(DRFLAC_SUPPORT_SSE41) && !defined(DRFLAC_NO_SSE41) && __has_include() + #define DRFLAC_SUPPORT_SSE41 + #endif + #endif + + #if defined(DRFLAC_SUPPORT_SSE41) + #include + #elif defined(DRFLAC_SUPPORT_SSE2) + #include + #endif + #endif + + #if defined(DRFLAC_ARM) + #if !defined(DRFLAC_NO_NEON) && (defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64)) + #define DRFLAC_SUPPORT_NEON + #include + #endif + #endif +#endif + +/* Compile-time CPU feature support. */ +#if !defined(DR_FLAC_NO_SIMD) && (defined(DRFLAC_X86) || defined(DRFLAC_X64)) + #if defined(_MSC_VER) && !defined(__clang__) + #if _MSC_VER >= 1400 + #include + static void drflac__cpuid(int info[4], int fid) + { + __cpuid(info, fid); + } + #else + #define DRFLAC_NO_CPUID + #endif + #else + #if defined(__GNUC__) || defined(__clang__) + static void drflac__cpuid(int info[4], int fid) + { + /* + It looks like the -fPIC option uses the ebx register which GCC complains about. We can work around this by just using a different register, the + specific register of which I'm letting the compiler decide on. The "k" prefix is used to specify a 32-bit register. The {...} syntax is for + supporting different assembly dialects. + + What's basically happening is that we're saving and restoring the ebx register manually. + */ + #if defined(DRFLAC_X86) && defined(__PIC__) + __asm__ __volatile__ ( + "xchg{l} {%%}ebx, %k1;" + "cpuid;" + "xchg{l} {%%}ebx, %k1;" + : "=a"(info[0]), "=&r"(info[1]), "=c"(info[2]), "=d"(info[3]) : "a"(fid), "c"(0) + ); + #else + __asm__ __volatile__ ( + "cpuid" : "=a"(info[0]), "=b"(info[1]), "=c"(info[2]), "=d"(info[3]) : "a"(fid), "c"(0) + ); + #endif + } + #else + #define DRFLAC_NO_CPUID + #endif + #endif +#else + #define DRFLAC_NO_CPUID +#endif + +static DRFLAC_INLINE drflac_bool32 drflac_has_sse2(void) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + #if (defined(DRFLAC_X64) || defined(DRFLAC_X86)) && !defined(DRFLAC_NO_SSE2) + #if defined(DRFLAC_X64) + return DRFLAC_TRUE; /* 64-bit targets always support SSE2. */ + #elif (defined(_M_IX86_FP) && _M_IX86_FP == 2) || defined(__SSE2__) + return DRFLAC_TRUE; /* If the compiler is allowed to freely generate SSE2 code we can assume support. */ + #else + #if defined(DRFLAC_NO_CPUID) + return DRFLAC_FALSE; + #else + int info[4]; + drflac__cpuid(info, 1); + return (info[3] & (1 << 26)) != 0; + #endif + #endif + #else + return DRFLAC_FALSE; /* SSE2 is only supported on x86 and x64 architectures. */ + #endif +#else + return DRFLAC_FALSE; /* No compiler support. */ +#endif +} + +static DRFLAC_INLINE drflac_bool32 drflac_has_sse41(void) +{ +#if defined(DRFLAC_SUPPORT_SSE41) + #if (defined(DRFLAC_X64) || defined(DRFLAC_X86)) && !defined(DRFLAC_NO_SSE41) + #if defined(__SSE4_1__) || defined(__AVX__) + return DRFLAC_TRUE; /* If the compiler is allowed to freely generate SSE41 code we can assume support. */ + #else + #if defined(DRFLAC_NO_CPUID) + return DRFLAC_FALSE; + #else + int info[4]; + drflac__cpuid(info, 1); + return (info[2] & (1 << 19)) != 0; + #endif + #endif + #else + return DRFLAC_FALSE; /* SSE41 is only supported on x86 and x64 architectures. */ + #endif +#else + return DRFLAC_FALSE; /* No compiler support. */ +#endif +} + + +#if defined(_MSC_VER) && _MSC_VER >= 1500 && (defined(DRFLAC_X86) || defined(DRFLAC_X64)) && !defined(__clang__) + #define DRFLAC_HAS_LZCNT_INTRINSIC +#elif (defined(__GNUC__) && ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7))) + #define DRFLAC_HAS_LZCNT_INTRINSIC +#elif defined(__clang__) + #if defined(__has_builtin) + #if __has_builtin(__builtin_clzll) || __has_builtin(__builtin_clzl) + #define DRFLAC_HAS_LZCNT_INTRINSIC + #endif + #endif +#endif + +#if defined(_MSC_VER) && _MSC_VER >= 1400 && !defined(__clang__) + #define DRFLAC_HAS_BYTESWAP16_INTRINSIC + #define DRFLAC_HAS_BYTESWAP32_INTRINSIC + #define DRFLAC_HAS_BYTESWAP64_INTRINSIC +#elif defined(__clang__) + #if defined(__has_builtin) + #if __has_builtin(__builtin_bswap16) + #define DRFLAC_HAS_BYTESWAP16_INTRINSIC + #endif + #if __has_builtin(__builtin_bswap32) + #define DRFLAC_HAS_BYTESWAP32_INTRINSIC + #endif + #if __has_builtin(__builtin_bswap64) + #define DRFLAC_HAS_BYTESWAP64_INTRINSIC + #endif + #endif +#elif defined(__GNUC__) + #if ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)) + #define DRFLAC_HAS_BYTESWAP32_INTRINSIC + #define DRFLAC_HAS_BYTESWAP64_INTRINSIC + #endif + #if ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)) + #define DRFLAC_HAS_BYTESWAP16_INTRINSIC + #endif +#elif defined(__WATCOMC__) && defined(__386__) + #define DRFLAC_HAS_BYTESWAP16_INTRINSIC + #define DRFLAC_HAS_BYTESWAP32_INTRINSIC + #define DRFLAC_HAS_BYTESWAP64_INTRINSIC + extern __inline drflac_uint16 _watcom_bswap16(drflac_uint16); + extern __inline drflac_uint32 _watcom_bswap32(drflac_uint32); + extern __inline drflac_uint64 _watcom_bswap64(drflac_uint64); +#pragma aux _watcom_bswap16 = \ + "xchg al, ah" \ + parm [ax] \ + value [ax] \ + modify nomemory; +#pragma aux _watcom_bswap32 = \ + "bswap eax" \ + parm [eax] \ + value [eax] \ + modify nomemory; +#pragma aux _watcom_bswap64 = \ + "bswap eax" \ + "bswap edx" \ + "xchg eax,edx" \ + parm [eax edx] \ + value [eax edx] \ + modify nomemory; +#endif + + +/* Standard library stuff. */ +#ifndef DRFLAC_ASSERT +#include +#define DRFLAC_ASSERT(expression) assert(expression) +#endif +#ifndef DRFLAC_MALLOC +#define DRFLAC_MALLOC(sz) malloc((sz)) +#endif +#ifndef DRFLAC_REALLOC +#define DRFLAC_REALLOC(p, sz) realloc((p), (sz)) +#endif +#ifndef DRFLAC_FREE +#define DRFLAC_FREE(p) free((p)) +#endif +#ifndef DRFLAC_COPY_MEMORY +#define DRFLAC_COPY_MEMORY(dst, src, sz) memcpy((dst), (src), (sz)) +#endif +#ifndef DRFLAC_ZERO_MEMORY +#define DRFLAC_ZERO_MEMORY(p, sz) memset((p), 0, (sz)) +#endif +#ifndef DRFLAC_ZERO_OBJECT +#define DRFLAC_ZERO_OBJECT(p) DRFLAC_ZERO_MEMORY((p), sizeof(*(p))) +#endif + +#define DRFLAC_MAX_SIMD_VECTOR_SIZE 64 /* 64 for AVX-512 in the future. */ + +/* Result Codes */ +typedef drflac_int32 drflac_result; +#define DRFLAC_SUCCESS 0 +#define DRFLAC_ERROR -1 /* A generic error. */ +#define DRFLAC_INVALID_ARGS -2 +#define DRFLAC_INVALID_OPERATION -3 +#define DRFLAC_OUT_OF_MEMORY -4 +#define DRFLAC_OUT_OF_RANGE -5 +#define DRFLAC_ACCESS_DENIED -6 +#define DRFLAC_DOES_NOT_EXIST -7 +#define DRFLAC_ALREADY_EXISTS -8 +#define DRFLAC_TOO_MANY_OPEN_FILES -9 +#define DRFLAC_INVALID_FILE -10 +#define DRFLAC_TOO_BIG -11 +#define DRFLAC_PATH_TOO_LONG -12 +#define DRFLAC_NAME_TOO_LONG -13 +#define DRFLAC_NOT_DIRECTORY -14 +#define DRFLAC_IS_DIRECTORY -15 +#define DRFLAC_DIRECTORY_NOT_EMPTY -16 +#define DRFLAC_END_OF_FILE -17 +#define DRFLAC_NO_SPACE -18 +#define DRFLAC_BUSY -19 +#define DRFLAC_IO_ERROR -20 +#define DRFLAC_INTERRUPT -21 +#define DRFLAC_UNAVAILABLE -22 +#define DRFLAC_ALREADY_IN_USE -23 +#define DRFLAC_BAD_ADDRESS -24 +#define DRFLAC_BAD_SEEK -25 +#define DRFLAC_BAD_PIPE -26 +#define DRFLAC_DEADLOCK -27 +#define DRFLAC_TOO_MANY_LINKS -28 +#define DRFLAC_NOT_IMPLEMENTED -29 +#define DRFLAC_NO_MESSAGE -30 +#define DRFLAC_BAD_MESSAGE -31 +#define DRFLAC_NO_DATA_AVAILABLE -32 +#define DRFLAC_INVALID_DATA -33 +#define DRFLAC_TIMEOUT -34 +#define DRFLAC_NO_NETWORK -35 +#define DRFLAC_NOT_UNIQUE -36 +#define DRFLAC_NOT_SOCKET -37 +#define DRFLAC_NO_ADDRESS -38 +#define DRFLAC_BAD_PROTOCOL -39 +#define DRFLAC_PROTOCOL_UNAVAILABLE -40 +#define DRFLAC_PROTOCOL_NOT_SUPPORTED -41 +#define DRFLAC_PROTOCOL_FAMILY_NOT_SUPPORTED -42 +#define DRFLAC_ADDRESS_FAMILY_NOT_SUPPORTED -43 +#define DRFLAC_SOCKET_NOT_SUPPORTED -44 +#define DRFLAC_CONNECTION_RESET -45 +#define DRFLAC_ALREADY_CONNECTED -46 +#define DRFLAC_NOT_CONNECTED -47 +#define DRFLAC_CONNECTION_REFUSED -48 +#define DRFLAC_NO_HOST -49 +#define DRFLAC_IN_PROGRESS -50 +#define DRFLAC_CANCELLED -51 +#define DRFLAC_MEMORY_ALREADY_MAPPED -52 +#define DRFLAC_AT_END -53 + +#define DRFLAC_CRC_MISMATCH -100 +/* End Result Codes */ -#define DRFLAC_BLOCK_TYPE_STREAMINFO 0 -#define DRFLAC_BLOCK_TYPE_PADDING 1 -#define DRFLAC_BLOCK_TYPE_APPLICATION 2 -#define DRFLAC_BLOCK_TYPE_SEEKTABLE 3 -#define DRFLAC_BLOCK_TYPE_VORBIS_COMMENT 4 -#define DRFLAC_BLOCK_TYPE_CUESHEET 5 -#define DRFLAC_BLOCK_TYPE_PICTURE 6 -#define DRFLAC_BLOCK_TYPE_INVALID 127 #define DRFLAC_SUBFRAME_CONSTANT 0 #define DRFLAC_SUBFRAME_VERBATIM 1 @@ -359,154 +1633,169 @@ static drflac* drflac_open_memory(const void* data, size_t dataSize); #define DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE 9 #define DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE 10 -typedef struct -{ - uint64_t firstSample; - uint64_t frameOffset; // The offset from the first byte of the header of the first frame. - uint16_t sampleCount; -} drflac_seekpoint; +#define DRFLAC_SEEKPOINT_SIZE_IN_BYTES 18 +#define DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES 36 +#define DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES 12 -#ifndef DR_FLAC_NO_STDIO -#if defined(DR_FLAC_NO_WIN32_IO) || !defined(_WIN32) -#include +#define drflac_align(x, a) ((((x) + (a) - 1) / (a)) * (a)) -static size_t drflac__on_read_stdio(void* pUserData, void* bufferOut, size_t bytesToRead) -{ - return fread(bufferOut, 1, bytesToRead, (FILE*)pUserData); -} -static bool drflac__on_seek_stdio(void* pUserData, int offset) +DRFLAC_API void drflac_version(drflac_uint32* pMajor, drflac_uint32* pMinor, drflac_uint32* pRevision) { - return fseek((FILE*)pUserData, offset, SEEK_CUR) == 0; -} - -drflac* drflac_open_file(const char* filename) -{ - FILE* pFile; -#ifdef _MSC_VER - if (fopen_s(&pFile, filename, "rb") != 0) { - return NULL; + if (pMajor) { + *pMajor = DRFLAC_VERSION_MAJOR; } + + if (pMinor) { + *pMinor = DRFLAC_VERSION_MINOR; + } + + if (pRevision) { + *pRevision = DRFLAC_VERSION_REVISION; + } +} + +DRFLAC_API const char* drflac_version_string(void) +{ + return DRFLAC_VERSION_STRING; +} + + +/* CPU caps. */ +#if defined(__has_feature) + #if __has_feature(thread_sanitizer) + #define DRFLAC_NO_THREAD_SANITIZE __attribute__((no_sanitize("thread"))) + #else + #define DRFLAC_NO_THREAD_SANITIZE + #endif #else - pFile = fopen(filename, "rb"); - if (pFile == NULL) { - return NULL; - } + #define DRFLAC_NO_THREAD_SANITIZE #endif - return drflac_open(drflac__on_read_stdio, drflac__on_seek_stdio, pFile); +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) +static drflac_bool32 drflac__gIsLZCNTSupported = DRFLAC_FALSE; +#endif + +#ifndef DRFLAC_NO_CPUID +static drflac_bool32 drflac__gIsSSE2Supported = DRFLAC_FALSE; +static drflac_bool32 drflac__gIsSSE41Supported = DRFLAC_FALSE; + +/* +I've had a bug report that Clang's ThreadSanitizer presents a warning in this function. Having reviewed this, this does +actually make sense. However, since CPU caps should never differ for a running process, I don't think the trade off of +complicating internal API's by passing around CPU caps versus just disabling the warnings is worthwhile. I'm therefore +just going to disable these warnings. This is disabled via the DRFLAC_NO_THREAD_SANITIZE attribute. +*/ +DRFLAC_NO_THREAD_SANITIZE static void drflac__init_cpu_caps(void) +{ + static drflac_bool32 isCPUCapsInitialized = DRFLAC_FALSE; + + if (!isCPUCapsInitialized) { + /* LZCNT */ +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) + int info[4] = {0}; + drflac__cpuid(info, 0x80000001); + drflac__gIsLZCNTSupported = (info[2] & (1 << 5)) != 0; +#endif + + /* SSE2 */ + drflac__gIsSSE2Supported = drflac_has_sse2(); + + /* SSE4.1 */ + drflac__gIsSSE41Supported = drflac_has_sse41(); + + /* Initialized. */ + isCPUCapsInitialized = DRFLAC_TRUE; + } } #else -#include +static drflac_bool32 drflac__gIsNEONSupported = DRFLAC_FALSE; -static size_t drflac__on_read_stdio(void* pUserData, void* bufferOut, size_t bytesToRead) +static DRFLAC_INLINE drflac_bool32 drflac__has_neon(void) { - assert(bytesToRead < 0xFFFFFFFF); // dr_flac will never request huge amounts of data at a time. This is a safe assertion. - - DWORD bytesRead; - ReadFile((HANDLE)pUserData, bufferOut, (DWORD)bytesToRead, &bytesRead, NULL); - - return (size_t)bytesRead; +#if defined(DRFLAC_SUPPORT_NEON) + #if defined(DRFLAC_ARM) && !defined(DRFLAC_NO_NEON) + #if (defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64)) + return DRFLAC_TRUE; /* If the compiler is allowed to freely generate NEON code we can assume support. */ + #else + /* TODO: Runtime check. */ + return DRFLAC_FALSE; + #endif + #else + return DRFLAC_FALSE; /* NEON is only supported on ARM architectures. */ + #endif +#else + return DRFLAC_FALSE; /* No compiler support. */ +#endif } -static bool drflac__on_seek_stdio(void* pUserData, int offset) +DRFLAC_NO_THREAD_SANITIZE static void drflac__init_cpu_caps(void) { - return SetFilePointer((HANDLE)pUserData, offset, NULL, FILE_CURRENT) != INVALID_SET_FILE_POINTER; -} + drflac__gIsNEONSupported = drflac__has_neon(); -static drflac* drflac_open_file(const char* filename) -{ - HANDLE hFile = CreateFileA(filename, FILE_GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (hFile == INVALID_HANDLE_VALUE) { - return NULL; - } - - return drflac_open(drflac__on_read_stdio, drflac__on_seek_stdio, (void*)hFile); +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) && defined(DRFLAC_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 5) + drflac__gIsLZCNTSupported = DRFLAC_TRUE; +#endif } #endif -#endif //DR_FLAC_NO_STDIO -typedef struct -{ - /// A pointer to the beginning of the data. We use a char as the type here for easy offsetting. - const unsigned char* data; - - /// The size of the data. - size_t dataSize; - - /// The position we're currently sitting at. - size_t currentReadPos; - -} drflac_memory; - -static size_t drflac__on_read_memory(void* pUserData, void* bufferOut, size_t bytesToRead) -{ - drflac_memory* memory = (drflac_memory*)pUserData; - assert(memory != NULL); - assert(memory->dataSize >= memory->currentReadPos); - - size_t bytesRemaining = memory->dataSize - memory->currentReadPos; - if (bytesToRead > bytesRemaining) { - bytesToRead = bytesRemaining; - } - - if (bytesToRead > 0) { - memcpy(bufferOut, memory->data + memory->currentReadPos, bytesToRead); - memory->currentReadPos += bytesToRead; - } - - return bytesToRead; -} - -static bool drflac__on_seek_memory(void* pUserData, int offset) -{ - drflac_memory* memory = (drflac_memory*)pUserData; - assert(memory != NULL); - - if (offset > 0) { - if (memory->currentReadPos + offset > memory->dataSize) { - offset = (int)(memory->dataSize - memory->currentReadPos); // Trying to seek too far forward. - } - } else { - if (memory->currentReadPos < (size_t)-offset) { - offset = -(int)memory->currentReadPos; // Trying to seek too far backwards. - } - } - - // This will never underflow thanks to the clamps above. - memory->currentReadPos += offset; - - return 1; -} - -static drflac* drflac_open_memory(const void* data, size_t dataSize) -{ - drflac_memory* pUserData = (drflac_memory*)malloc(sizeof(*pUserData)); - if (pUserData == NULL) { - return NULL; - } - - pUserData->data = (const unsigned char*)data; - pUserData->dataSize = dataSize; - pUserData->currentReadPos = 0; - return drflac_open(drflac__on_read_memory, drflac__on_seek_memory, pUserData); -} - - -//// Endian Management //// -static DRFLAC_INLINE bool drflac__is_little_endian() +/* Endian Management */ +static DRFLAC_INLINE drflac_bool32 drflac__is_little_endian(void) { +#if defined(DRFLAC_X86) || defined(DRFLAC_X64) + return DRFLAC_TRUE; +#elif defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && __BYTE_ORDER == __LITTLE_ENDIAN + return DRFLAC_TRUE; +#else int n = 1; return (*(char*)&n) == 1; +#endif } -static DRFLAC_INLINE uint32_t drflac__swap_endian_uint32(uint32_t n) +static DRFLAC_INLINE drflac_uint16 drflac__swap_endian_uint16(drflac_uint16 n) { -#ifdef _MSC_VER - return _byteswap_ulong(n); -#elif defined(__GNUC__) && ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)) - return __builtin_bswap32(n); +#ifdef DRFLAC_HAS_BYTESWAP16_INTRINSIC + #if defined(_MSC_VER) && !defined(__clang__) + return _byteswap_ushort(n); + #elif defined(__GNUC__) || defined(__clang__) + return __builtin_bswap16(n); + #elif defined(__WATCOMC__) && defined(__386__) + return _watcom_bswap16(n); + #else + #error "This compiler does not support the byte swap intrinsic." + #endif +#else + return ((n & 0xFF00) >> 8) | + ((n & 0x00FF) << 8); +#endif +} + +static DRFLAC_INLINE drflac_uint32 drflac__swap_endian_uint32(drflac_uint32 n) +{ +#ifdef DRFLAC_HAS_BYTESWAP32_INTRINSIC + #if defined(_MSC_VER) && !defined(__clang__) + return _byteswap_ulong(n); + #elif defined(__GNUC__) || defined(__clang__) + #if defined(DRFLAC_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 6) && !defined(__ARM_ARCH_6M__) && !defined(DRFLAC_64BIT) /* <-- 64-bit inline assembly has not been tested, so disabling for now. */ + /* Inline assembly optimized implementation for ARM. In my testing, GCC does not generate optimized code with __builtin_bswap32(). */ + drflac_uint32 r; + __asm__ __volatile__ ( + #if defined(DRFLAC_64BIT) + "rev %w[out], %w[in]" : [out]"=r"(r) : [in]"r"(n) /* <-- This is untested. If someone in the community could test this, that would be appreciated! */ + #else + "rev %[out], %[in]" : [out]"=r"(r) : [in]"r"(n) + #endif + ); + return r; + #else + return __builtin_bswap32(n); + #endif + #elif defined(__WATCOMC__) && defined(__386__) + return _watcom_bswap32(n); + #else + #error "This compiler does not support the byte swap intrinsic." + #endif #else return ((n & 0xFF000000) >> 24) | ((n & 0x00FF0000) >> 8) | @@ -515,399 +1804,850 @@ static DRFLAC_INLINE uint32_t drflac__swap_endian_uint32(uint32_t n) #endif } -static DRFLAC_INLINE uint64_t drflac__swap_endian_uint64(uint64_t n) +static DRFLAC_INLINE drflac_uint64 drflac__swap_endian_uint64(drflac_uint64 n) { -#ifdef _MSC_VER - return _byteswap_uint64(n); -#elif defined(__GNUC__) && ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)) - return __builtin_bswap64(n); +#ifdef DRFLAC_HAS_BYTESWAP64_INTRINSIC + #if defined(_MSC_VER) && !defined(__clang__) + return _byteswap_uint64(n); + #elif defined(__GNUC__) || defined(__clang__) + return __builtin_bswap64(n); + #elif defined(__WATCOMC__) && defined(__386__) + return _watcom_bswap64(n); + #else + #error "This compiler does not support the byte swap intrinsic." + #endif #else - return ((n & 0xFF00000000000000ULL) >> 56) | - ((n & 0x00FF000000000000ULL) >> 40) | - ((n & 0x0000FF0000000000ULL) >> 24) | - ((n & 0x000000FF00000000ULL) >> 8) | - ((n & 0x00000000FF000000ULL) << 8) | - ((n & 0x0000000000FF0000ULL) << 24) | - ((n & 0x000000000000FF00ULL) << 40) | - ((n & 0x00000000000000FFULL) << 56); + /* Weird "<< 32" bitshift is required for C89 because it doesn't support 64-bit constants. Should be optimized out by a good compiler. */ + return ((n & ((drflac_uint64)0xFF000000 << 32)) >> 56) | + ((n & ((drflac_uint64)0x00FF0000 << 32)) >> 40) | + ((n & ((drflac_uint64)0x0000FF00 << 32)) >> 24) | + ((n & ((drflac_uint64)0x000000FF << 32)) >> 8) | + ((n & ((drflac_uint64)0xFF000000 )) << 8) | + ((n & ((drflac_uint64)0x00FF0000 )) << 24) | + ((n & ((drflac_uint64)0x0000FF00 )) << 40) | + ((n & ((drflac_uint64)0x000000FF )) << 56); #endif } -static DRFLAC_INLINE uint32_t drflac__be2host_32(uint32_t n) +static DRFLAC_INLINE drflac_uint16 drflac__be2host_16(drflac_uint16 n) +{ + if (drflac__is_little_endian()) { + return drflac__swap_endian_uint16(n); + } + + return n; +} + +static DRFLAC_INLINE drflac_uint32 drflac__be2host_32(drflac_uint32 n) { -#if defined(__BYTE_ORDER__) && (__BYTE_ORDER == __ORDER_LITTLE_ENDIAN__) - return drflac__swap_endian_uint32(n); -#elif defined(__linux__) - return be32toh(n); -#else if (drflac__is_little_endian()) { return drflac__swap_endian_uint32(n); } return n; -#endif } -static DRFLAC_INLINE uint64_t drflac__be2host_64(uint64_t n) +static DRFLAC_INLINE drflac_uint32 drflac__be2host_32_ptr_unaligned(const void* pData) +{ + const drflac_uint8* pNum = (drflac_uint8*)pData; + return *(pNum) << 24 | *(pNum+1) << 16 | *(pNum+2) << 8 | *(pNum+3); +} + +static DRFLAC_INLINE drflac_uint64 drflac__be2host_64(drflac_uint64 n) { -#if defined(__BYTE_ORDER__) && (__BYTE_ORDER == __ORDER_LITTLE_ENDIAN__) - return drflac__swap_endian_uint64(n); -#elif defined(__linux__) - return be64toh(n); -#else if (drflac__is_little_endian()) { return drflac__swap_endian_uint64(n); } return n; +} + + +static DRFLAC_INLINE drflac_uint32 drflac__le2host_32(drflac_uint32 n) +{ + if (!drflac__is_little_endian()) { + return drflac__swap_endian_uint32(n); + } + + return n; +} + +static DRFLAC_INLINE drflac_uint32 drflac__le2host_32_ptr_unaligned(const void* pData) +{ + const drflac_uint8* pNum = (drflac_uint8*)pData; + return *pNum | *(pNum+1) << 8 | *(pNum+2) << 16 | *(pNum+3) << 24; +} + + +static DRFLAC_INLINE drflac_uint32 drflac__unsynchsafe_32(drflac_uint32 n) +{ + drflac_uint32 result = 0; + result |= (n & 0x7F000000) >> 3; + result |= (n & 0x007F0000) >> 2; + result |= (n & 0x00007F00) >> 1; + result |= (n & 0x0000007F) >> 0; + + return result; +} + + + +/* The CRC code below is based on this document: http://zlib.net/crc_v3.txt */ +static drflac_uint8 drflac__crc8_table[] = { + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, + 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, + 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD, + 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD, + 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, + 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, + 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, + 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, + 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, + 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, + 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, + 0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, + 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, + 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, + 0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3 +}; + +static drflac_uint16 drflac__crc16_table[] = { + 0x0000, 0x8005, 0x800F, 0x000A, 0x801B, 0x001E, 0x0014, 0x8011, + 0x8033, 0x0036, 0x003C, 0x8039, 0x0028, 0x802D, 0x8027, 0x0022, + 0x8063, 0x0066, 0x006C, 0x8069, 0x0078, 0x807D, 0x8077, 0x0072, + 0x0050, 0x8055, 0x805F, 0x005A, 0x804B, 0x004E, 0x0044, 0x8041, + 0x80C3, 0x00C6, 0x00CC, 0x80C9, 0x00D8, 0x80DD, 0x80D7, 0x00D2, + 0x00F0, 0x80F5, 0x80FF, 0x00FA, 0x80EB, 0x00EE, 0x00E4, 0x80E1, + 0x00A0, 0x80A5, 0x80AF, 0x00AA, 0x80BB, 0x00BE, 0x00B4, 0x80B1, + 0x8093, 0x0096, 0x009C, 0x8099, 0x0088, 0x808D, 0x8087, 0x0082, + 0x8183, 0x0186, 0x018C, 0x8189, 0x0198, 0x819D, 0x8197, 0x0192, + 0x01B0, 0x81B5, 0x81BF, 0x01BA, 0x81AB, 0x01AE, 0x01A4, 0x81A1, + 0x01E0, 0x81E5, 0x81EF, 0x01EA, 0x81FB, 0x01FE, 0x01F4, 0x81F1, + 0x81D3, 0x01D6, 0x01DC, 0x81D9, 0x01C8, 0x81CD, 0x81C7, 0x01C2, + 0x0140, 0x8145, 0x814F, 0x014A, 0x815B, 0x015E, 0x0154, 0x8151, + 0x8173, 0x0176, 0x017C, 0x8179, 0x0168, 0x816D, 0x8167, 0x0162, + 0x8123, 0x0126, 0x012C, 0x8129, 0x0138, 0x813D, 0x8137, 0x0132, + 0x0110, 0x8115, 0x811F, 0x011A, 0x810B, 0x010E, 0x0104, 0x8101, + 0x8303, 0x0306, 0x030C, 0x8309, 0x0318, 0x831D, 0x8317, 0x0312, + 0x0330, 0x8335, 0x833F, 0x033A, 0x832B, 0x032E, 0x0324, 0x8321, + 0x0360, 0x8365, 0x836F, 0x036A, 0x837B, 0x037E, 0x0374, 0x8371, + 0x8353, 0x0356, 0x035C, 0x8359, 0x0348, 0x834D, 0x8347, 0x0342, + 0x03C0, 0x83C5, 0x83CF, 0x03CA, 0x83DB, 0x03DE, 0x03D4, 0x83D1, + 0x83F3, 0x03F6, 0x03FC, 0x83F9, 0x03E8, 0x83ED, 0x83E7, 0x03E2, + 0x83A3, 0x03A6, 0x03AC, 0x83A9, 0x03B8, 0x83BD, 0x83B7, 0x03B2, + 0x0390, 0x8395, 0x839F, 0x039A, 0x838B, 0x038E, 0x0384, 0x8381, + 0x0280, 0x8285, 0x828F, 0x028A, 0x829B, 0x029E, 0x0294, 0x8291, + 0x82B3, 0x02B6, 0x02BC, 0x82B9, 0x02A8, 0x82AD, 0x82A7, 0x02A2, + 0x82E3, 0x02E6, 0x02EC, 0x82E9, 0x02F8, 0x82FD, 0x82F7, 0x02F2, + 0x02D0, 0x82D5, 0x82DF, 0x02DA, 0x82CB, 0x02CE, 0x02C4, 0x82C1, + 0x8243, 0x0246, 0x024C, 0x8249, 0x0258, 0x825D, 0x8257, 0x0252, + 0x0270, 0x8275, 0x827F, 0x027A, 0x826B, 0x026E, 0x0264, 0x8261, + 0x0220, 0x8225, 0x822F, 0x022A, 0x823B, 0x023E, 0x0234, 0x8231, + 0x8213, 0x0216, 0x021C, 0x8219, 0x0208, 0x820D, 0x8207, 0x0202 +}; + +static DRFLAC_INLINE drflac_uint8 drflac_crc8_byte(drflac_uint8 crc, drflac_uint8 data) +{ + return drflac__crc8_table[crc ^ data]; +} + +static DRFLAC_INLINE drflac_uint8 drflac_crc8(drflac_uint8 crc, drflac_uint32 data, drflac_uint32 count) +{ +#ifdef DR_FLAC_NO_CRC + (void)crc; + (void)data; + (void)count; + return 0; +#else +#if 0 + /* REFERENCE (use of this implementation requires an explicit flush by doing "drflac_crc8(crc, 0, 8);") */ + drflac_uint8 p = 0x07; + for (int i = count-1; i >= 0; --i) { + drflac_uint8 bit = (data & (1 << i)) >> i; + if (crc & 0x80) { + crc = ((crc << 1) | bit) ^ p; + } else { + crc = ((crc << 1) | bit); + } + } + return crc; +#else + drflac_uint32 wholeBytes; + drflac_uint32 leftoverBits; + drflac_uint64 leftoverDataMask; + + static drflac_uint64 leftoverDataMaskTable[8] = { + 0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F + }; + + DRFLAC_ASSERT(count <= 32); + + wholeBytes = count >> 3; + leftoverBits = count - (wholeBytes*8); + leftoverDataMask = leftoverDataMaskTable[leftoverBits]; + + switch (wholeBytes) { + case 4: crc = drflac_crc8_byte(crc, (drflac_uint8)((data & (0xFF000000UL << leftoverBits)) >> (24 + leftoverBits))); + case 3: crc = drflac_crc8_byte(crc, (drflac_uint8)((data & (0x00FF0000UL << leftoverBits)) >> (16 + leftoverBits))); + case 2: crc = drflac_crc8_byte(crc, (drflac_uint8)((data & (0x0000FF00UL << leftoverBits)) >> ( 8 + leftoverBits))); + case 1: crc = drflac_crc8_byte(crc, (drflac_uint8)((data & (0x000000FFUL << leftoverBits)) >> ( 0 + leftoverBits))); + case 0: if (leftoverBits > 0) crc = (drflac_uint8)((crc << leftoverBits) ^ drflac__crc8_table[(crc >> (8 - leftoverBits)) ^ (data & leftoverDataMask)]); + } + return crc; +#endif #endif } +static DRFLAC_INLINE drflac_uint16 drflac_crc16_byte(drflac_uint16 crc, drflac_uint8 data) +{ + return (crc << 8) ^ drflac__crc16_table[(drflac_uint8)(crc >> 8) ^ data]; +} + +static DRFLAC_INLINE drflac_uint16 drflac_crc16_cache(drflac_uint16 crc, drflac_cache_t data) +{ +#ifdef DRFLAC_64BIT + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 56) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 48) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 40) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 32) & 0xFF)); +#endif + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 24) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 16) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 8) & 0xFF)); + crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 0) & 0xFF)); + + return crc; +} + +static DRFLAC_INLINE drflac_uint16 drflac_crc16_bytes(drflac_uint16 crc, drflac_cache_t data, drflac_uint32 byteCount) +{ + switch (byteCount) + { +#ifdef DRFLAC_64BIT + case 8: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 56) & 0xFF)); + case 7: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 48) & 0xFF)); + case 6: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 40) & 0xFF)); + case 5: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 32) & 0xFF)); +#endif + case 4: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 24) & 0xFF)); + case 3: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 16) & 0xFF)); + case 2: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 8) & 0xFF)); + case 1: crc = drflac_crc16_byte(crc, (drflac_uint8)((data >> 0) & 0xFF)); + } + + return crc; +} + +#if 0 +static DRFLAC_INLINE drflac_uint16 drflac_crc16__32bit(drflac_uint16 crc, drflac_uint32 data, drflac_uint32 count) +{ +#ifdef DR_FLAC_NO_CRC + (void)crc; + (void)data; + (void)count; + return 0; +#else +#if 0 + /* REFERENCE (use of this implementation requires an explicit flush by doing "drflac_crc16(crc, 0, 16);") */ + drflac_uint16 p = 0x8005; + for (int i = count-1; i >= 0; --i) { + drflac_uint16 bit = (data & (1ULL << i)) >> i; + if (r & 0x8000) { + r = ((r << 1) | bit) ^ p; + } else { + r = ((r << 1) | bit); + } + } + + return crc; +#else + drflac_uint32 wholeBytes; + drflac_uint32 leftoverBits; + drflac_uint64 leftoverDataMask; + + static drflac_uint64 leftoverDataMaskTable[8] = { + 0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F + }; + + DRFLAC_ASSERT(count <= 64); + + wholeBytes = count >> 3; + leftoverBits = count & 7; + leftoverDataMask = leftoverDataMaskTable[leftoverBits]; + + switch (wholeBytes) { + default: + case 4: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (0xFF000000UL << leftoverBits)) >> (24 + leftoverBits))); + case 3: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (0x00FF0000UL << leftoverBits)) >> (16 + leftoverBits))); + case 2: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (0x0000FF00UL << leftoverBits)) >> ( 8 + leftoverBits))); + case 1: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (0x000000FFUL << leftoverBits)) >> ( 0 + leftoverBits))); + case 0: if (leftoverBits > 0) crc = (crc << leftoverBits) ^ drflac__crc16_table[(crc >> (16 - leftoverBits)) ^ (data & leftoverDataMask)]; + } + return crc; +#endif +#endif +} + +static DRFLAC_INLINE drflac_uint16 drflac_crc16__64bit(drflac_uint16 crc, drflac_uint64 data, drflac_uint32 count) +{ +#ifdef DR_FLAC_NO_CRC + (void)crc; + (void)data; + (void)count; + return 0; +#else + drflac_uint32 wholeBytes; + drflac_uint32 leftoverBits; + drflac_uint64 leftoverDataMask; + + static drflac_uint64 leftoverDataMaskTable[8] = { + 0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F + }; + + DRFLAC_ASSERT(count <= 64); + + wholeBytes = count >> 3; + leftoverBits = count & 7; + leftoverDataMask = leftoverDataMaskTable[leftoverBits]; + + switch (wholeBytes) { + default: + case 8: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0xFF000000 << 32) << leftoverBits)) >> (56 + leftoverBits))); /* Weird "<< 32" bitshift is required for C89 because it doesn't support 64-bit constants. Should be optimized out by a good compiler. */ + case 7: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x00FF0000 << 32) << leftoverBits)) >> (48 + leftoverBits))); + case 6: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x0000FF00 << 32) << leftoverBits)) >> (40 + leftoverBits))); + case 5: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x000000FF << 32) << leftoverBits)) >> (32 + leftoverBits))); + case 4: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0xFF000000 ) << leftoverBits)) >> (24 + leftoverBits))); + case 3: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x00FF0000 ) << leftoverBits)) >> (16 + leftoverBits))); + case 2: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x0000FF00 ) << leftoverBits)) >> ( 8 + leftoverBits))); + case 1: crc = drflac_crc16_byte(crc, (drflac_uint8)((data & (((drflac_uint64)0x000000FF ) << leftoverBits)) >> ( 0 + leftoverBits))); + case 0: if (leftoverBits > 0) crc = (crc << leftoverBits) ^ drflac__crc16_table[(crc >> (16 - leftoverBits)) ^ (data & leftoverDataMask)]; + } + return crc; +#endif +} + + +static DRFLAC_INLINE drflac_uint16 drflac_crc16(drflac_uint16 crc, drflac_cache_t data, drflac_uint32 count) +{ +#ifdef DRFLAC_64BIT + return drflac_crc16__64bit(crc, data, count); +#else + return drflac_crc16__32bit(crc, data, count); +#endif +} +#endif + + #ifdef DRFLAC_64BIT #define drflac__be2host__cache_line drflac__be2host_64 #else #define drflac__be2host__cache_line drflac__be2host_32 #endif +/* +BIT READING ATTEMPT #2 -// BIT READING ATTEMPT #2 -// -// This uses a 32- or 64-bit bit-shifted cache - as bits are read, the cache is shifted such that the first valid bit is sitting -// on the most significant bit. It uses the notion of an L1 and L2 cache (borrowed from CPU architecture), where the L1 cache -// is a 32- or 64-bit unsigned integer (depending on whether or not a 32- or 64-bit build is being compiled) and the L2 is an -// array of "cache lines", with each cache line being the same size as the L1. The L2 is a buffer of about 4KB and is where data -// from onRead() is read into. -#define DRFLAC_CACHE_L1_SIZE_BYTES (sizeof(pFlac->cache)) -#define DRFLAC_CACHE_L1_SIZE_BITS (sizeof(pFlac->cache)*8) -#define DRFLAC_CACHE_L1_BITS_REMAINING (DRFLAC_CACHE_L1_SIZE_BITS - (pFlac->consumedBits)) -#ifdef DRFLAC_64BIT -#define DRFLAC_CACHE_L1_SELECTION_MASK(_bitCount) (~(((uint64_t)-1LL) >> (_bitCount))) -#else -#define DRFLAC_CACHE_L1_SELECTION_MASK(_bitCount) (~(((uint32_t)-1) >> (_bitCount))) +This uses a 32- or 64-bit bit-shifted cache - as bits are read, the cache is shifted such that the first valid bit is sitting +on the most significant bit. It uses the notion of an L1 and L2 cache (borrowed from CPU architecture), where the L1 cache +is a 32- or 64-bit unsigned integer (depending on whether or not a 32- or 64-bit build is being compiled) and the L2 is an +array of "cache lines", with each cache line being the same size as the L1. The L2 is a buffer of about 4KB and is where data +from onRead() is read into. +*/ +#define DRFLAC_CACHE_L1_SIZE_BYTES(bs) (sizeof((bs)->cache)) +#define DRFLAC_CACHE_L1_SIZE_BITS(bs) (sizeof((bs)->cache)*8) +#define DRFLAC_CACHE_L1_BITS_REMAINING(bs) (DRFLAC_CACHE_L1_SIZE_BITS(bs) - (bs)->consumedBits) +#define DRFLAC_CACHE_L1_SELECTION_MASK(_bitCount) (~((~(drflac_cache_t)0) >> (_bitCount))) +#define DRFLAC_CACHE_L1_SELECTION_SHIFT(bs, _bitCount) (DRFLAC_CACHE_L1_SIZE_BITS(bs) - (_bitCount)) +#define DRFLAC_CACHE_L1_SELECT(bs, _bitCount) (((bs)->cache) & DRFLAC_CACHE_L1_SELECTION_MASK(_bitCount)) +#define DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, _bitCount) (DRFLAC_CACHE_L1_SELECT((bs), (_bitCount)) >> DRFLAC_CACHE_L1_SELECTION_SHIFT((bs), (_bitCount))) +#define DRFLAC_CACHE_L1_SELECT_AND_SHIFT_SAFE(bs, _bitCount)(DRFLAC_CACHE_L1_SELECT((bs), (_bitCount)) >> (DRFLAC_CACHE_L1_SELECTION_SHIFT((bs), (_bitCount)) & (DRFLAC_CACHE_L1_SIZE_BITS(bs)-1))) +#define DRFLAC_CACHE_L2_SIZE_BYTES(bs) (sizeof((bs)->cacheL2)) +#define DRFLAC_CACHE_L2_LINE_COUNT(bs) (DRFLAC_CACHE_L2_SIZE_BYTES(bs) / sizeof((bs)->cacheL2[0])) +#define DRFLAC_CACHE_L2_LINES_REMAINING(bs) (DRFLAC_CACHE_L2_LINE_COUNT(bs) - (bs)->nextL2Line) + + +#ifndef DR_FLAC_NO_CRC +static DRFLAC_INLINE void drflac__reset_crc16(drflac_bs* bs) +{ + bs->crc16 = 0; + bs->crc16CacheIgnoredBytes = bs->consumedBits >> 3; +} + +static DRFLAC_INLINE void drflac__update_crc16(drflac_bs* bs) +{ + if (bs->crc16CacheIgnoredBytes == 0) { + bs->crc16 = drflac_crc16_cache(bs->crc16, bs->crc16Cache); + } else { + bs->crc16 = drflac_crc16_bytes(bs->crc16, bs->crc16Cache, DRFLAC_CACHE_L1_SIZE_BYTES(bs) - bs->crc16CacheIgnoredBytes); + bs->crc16CacheIgnoredBytes = 0; + } +} + +static DRFLAC_INLINE drflac_uint16 drflac__flush_crc16(drflac_bs* bs) +{ + /* We should never be flushing in a situation where we are not aligned on a byte boundary. */ + DRFLAC_ASSERT((DRFLAC_CACHE_L1_BITS_REMAINING(bs) & 7) == 0); + + /* + The bits that were read from the L1 cache need to be accumulated. The number of bytes needing to be accumulated is determined + by the number of bits that have been consumed. + */ + if (DRFLAC_CACHE_L1_BITS_REMAINING(bs) == 0) { + drflac__update_crc16(bs); + } else { + /* We only accumulate the consumed bits. */ + bs->crc16 = drflac_crc16_bytes(bs->crc16, bs->crc16Cache >> DRFLAC_CACHE_L1_BITS_REMAINING(bs), (bs->consumedBits >> 3) - bs->crc16CacheIgnoredBytes); + + /* + The bits that we just accumulated should never be accumulated again. We need to keep track of how many bytes were accumulated + so we can handle that later. + */ + bs->crc16CacheIgnoredBytes = bs->consumedBits >> 3; + } + + return bs->crc16; +} #endif -#define DRFLAC_CACHE_L1_SELECTION_SHIFT(_bitCount) (DRFLAC_CACHE_L1_SIZE_BITS - (_bitCount)) -#define DRFLAC_CACHE_L1_SELECT(_bitCount) ((pFlac->cache) & DRFLAC_CACHE_L1_SELECTION_MASK(_bitCount)) -#define DRFLAC_CACHE_L1_SELECT_AND_SHIFT(_bitCount) (DRFLAC_CACHE_L1_SELECT(_bitCount) >> DRFLAC_CACHE_L1_SELECTION_SHIFT(_bitCount)) -#define DRFLAC_CACHE_L2_SIZE_BYTES (sizeof(pFlac->cacheL2)) -#define DRFLAC_CACHE_L2_LINE_COUNT (DRFLAC_CACHE_L2_SIZE_BYTES / sizeof(pFlac->cacheL2[0])) -#define DRFLAC_CACHE_L2_LINES_REMAINING (DRFLAC_CACHE_L2_LINE_COUNT - pFlac->nextL2Line) -static DRFLAC_INLINE bool drflac__reload_l1_cache_from_l2(drflac* pFlac) +static DRFLAC_INLINE drflac_bool32 drflac__reload_l1_cache_from_l2(drflac_bs* bs) { - // Fast path. Try loading straight from L2. - if (pFlac->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT) { - pFlac->cache = pFlac->cacheL2[pFlac->nextL2Line++]; - return true; + size_t bytesRead; + size_t alignedL1LineCount; + + /* Fast path. Try loading straight from L2. */ + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + bs->cache = bs->cacheL2[bs->nextL2Line++]; + return DRFLAC_TRUE; } - // If we get here it means we've run out of data in the L2 cache. We'll need to fetch more from the client. - size_t bytesRead = pFlac->onRead(pFlac->pUserData, pFlac->cacheL2, DRFLAC_CACHE_L2_SIZE_BYTES); - pFlac->currentBytePos += bytesRead; + /* + If we get here it means we've run out of data in the L2 cache. We'll need to fetch more from the client, if there's + any left. + */ + if (bs->unalignedByteCount > 0) { + return DRFLAC_FALSE; /* If we have any unaligned bytes it means there's no more aligned bytes left in the client. */ + } - pFlac->nextL2Line = 0; - if (bytesRead == DRFLAC_CACHE_L2_SIZE_BYTES) { - pFlac->cache = pFlac->cacheL2[pFlac->nextL2Line++]; - return true; + bytesRead = bs->onRead(bs->pUserData, bs->cacheL2, DRFLAC_CACHE_L2_SIZE_BYTES(bs)); + + bs->nextL2Line = 0; + if (bytesRead == DRFLAC_CACHE_L2_SIZE_BYTES(bs)) { + bs->cache = bs->cacheL2[bs->nextL2Line++]; + return DRFLAC_TRUE; } - // If we get here it means we were unable to retrieve enough data to fill the entire L2 cache. It probably - // means we've just reached the end of the file. We need to move the valid data down to the end of the buffer - // and adjust the index of the next line accordingly. Also keep in mind that the L2 cache must be aligned to - // the size of the L1 so we'll need to seek backwards by any misaligned bytes. - size_t alignedL1LineCount = bytesRead / DRFLAC_CACHE_L1_SIZE_BYTES; - if (alignedL1LineCount > 0) - { - size_t offset = DRFLAC_CACHE_L2_LINE_COUNT - alignedL1LineCount; - for (size_t i = alignedL1LineCount; i > 0; --i) { - pFlac->cacheL2[i-1 + offset] = pFlac->cacheL2[i-1]; - } + /* + If we get here it means we were unable to retrieve enough data to fill the entire L2 cache. It probably + means we've just reached the end of the file. We need to move the valid data down to the end of the buffer + and adjust the index of the next line accordingly. Also keep in mind that the L2 cache must be aligned to + the size of the L1 so we'll need to seek backwards by any misaligned bytes. + */ + alignedL1LineCount = bytesRead / DRFLAC_CACHE_L1_SIZE_BYTES(bs); - pFlac->nextL2Line = offset; - pFlac->unusedL2Lines = offset; - - // At this point there may be some leftover unaligned bytes. We need to seek backwards so we don't lose - // those bytes. - size_t unalignedBytes = bytesRead - (alignedL1LineCount * DRFLAC_CACHE_L1_SIZE_BYTES); - if (unalignedBytes > 0) { - pFlac->onSeek(pFlac->pUserData, -(int)unalignedBytes); - pFlac->currentBytePos -= unalignedBytes; - } - - pFlac->cache = pFlac->cacheL2[pFlac->nextL2Line++]; - return true; + /* We need to keep track of any unaligned bytes for later use. */ + bs->unalignedByteCount = bytesRead - (alignedL1LineCount * DRFLAC_CACHE_L1_SIZE_BYTES(bs)); + if (bs->unalignedByteCount > 0) { + bs->unalignedCache = bs->cacheL2[alignedL1LineCount]; } - else - { - // If we get into this branch it means we weren't able to load any L1-aligned data. We just need to seek - // backwards by the leftover bytes and return false. - if (bytesRead > 0) { - pFlac->onSeek(pFlac->pUserData, -(int)bytesRead); - pFlac->currentBytePos -= bytesRead; + + if (alignedL1LineCount > 0) { + size_t offset = DRFLAC_CACHE_L2_LINE_COUNT(bs) - alignedL1LineCount; + size_t i; + for (i = alignedL1LineCount; i > 0; --i) { + bs->cacheL2[i-1 + offset] = bs->cacheL2[i-1]; } - pFlac->nextL2Line = DRFLAC_CACHE_L2_LINE_COUNT; - return false; + bs->nextL2Line = (drflac_uint32)offset; + bs->cache = bs->cacheL2[bs->nextL2Line++]; + return DRFLAC_TRUE; + } else { + /* If we get into this branch it means we weren't able to load any L1-aligned data. */ + bs->nextL2Line = DRFLAC_CACHE_L2_LINE_COUNT(bs); + return DRFLAC_FALSE; } } -static bool drflac__reload_cache(drflac* pFlac) +static drflac_bool32 drflac__reload_cache(drflac_bs* bs) { - // Fast path. Try just moving the next value in the L2 cache to the L1 cache. - if (drflac__reload_l1_cache_from_l2(pFlac)) { - pFlac->cache = drflac__be2host__cache_line(pFlac->cache); - pFlac->consumedBits = 0; - return true; + size_t bytesRead; + +#ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); +#endif + + /* Fast path. Try just moving the next value in the L2 cache to the L1 cache. */ + if (drflac__reload_l1_cache_from_l2(bs)) { + bs->cache = drflac__be2host__cache_line(bs->cache); + bs->consumedBits = 0; +#ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs->cache; +#endif + return DRFLAC_TRUE; } - // Slow path. + /* Slow path. */ - // If we get here it means we have failed to load the L1 cache from the L2. Likely we've just reached the end of the stream and the last - // few bytes did not meet the alignment requirements for the L2 cache. In this case we need to fall back to a slower path and read the - // data straight from the client into the L1 cache. This should only really happen once per stream so efficiency is not important. - size_t bytesRead = pFlac->onRead(pFlac->pUserData, &pFlac->cache, DRFLAC_CACHE_L1_SIZE_BYTES); + /* + If we get here it means we have failed to load the L1 cache from the L2. Likely we've just reached the end of the stream and the last + few bytes did not meet the alignment requirements for the L2 cache. In this case we need to fall back to a slower path and read the + data from the unaligned cache. + */ + bytesRead = bs->unalignedByteCount; if (bytesRead == 0) { - return false; + bs->consumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs); /* <-- The stream has been exhausted, so marked the bits as consumed. */ + return DRFLAC_FALSE; } - pFlac->currentBytePos += bytesRead; + DRFLAC_ASSERT(bytesRead < DRFLAC_CACHE_L1_SIZE_BYTES(bs)); + bs->consumedBits = (drflac_uint32)(DRFLAC_CACHE_L1_SIZE_BYTES(bs) - bytesRead) * 8; - assert(bytesRead < DRFLAC_CACHE_L1_SIZE_BYTES); - pFlac->consumedBits = (DRFLAC_CACHE_L1_SIZE_BYTES - bytesRead) * 8; + bs->cache = drflac__be2host__cache_line(bs->unalignedCache); + bs->cache &= DRFLAC_CACHE_L1_SELECTION_MASK(DRFLAC_CACHE_L1_BITS_REMAINING(bs)); /* <-- Make sure the consumed bits are always set to zero. Other parts of the library depend on this property. */ + bs->unalignedByteCount = 0; /* <-- At this point the unaligned bytes have been moved into the cache and we thus have no more unaligned bytes. */ - pFlac->cache = drflac__be2host__cache_line(pFlac->cache); - pFlac->cache &= DRFLAC_CACHE_L1_SELECTION_MASK(DRFLAC_CACHE_L1_SIZE_BITS - pFlac->consumedBits); // <-- Make sure the consumed bits are always set to zero. Other parts of the library depend on this property. - return true; +#ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs->cache >> bs->consumedBits; + bs->crc16CacheIgnoredBytes = bs->consumedBits >> 3; +#endif + return DRFLAC_TRUE; } -static bool drflac__seek_bits(drflac* pFlac, size_t bitsToSeek) +static void drflac__reset_cache(drflac_bs* bs) { - if (bitsToSeek <= DRFLAC_CACHE_L1_BITS_REMAINING) { - pFlac->consumedBits += bitsToSeek; - pFlac->cache <<= bitsToSeek; - return true; - } else { - // It straddles the cached data. This function isn't called too frequently so I'm favouring simplicity here. - bitsToSeek -= DRFLAC_CACHE_L1_BITS_REMAINING; - pFlac->consumedBits += DRFLAC_CACHE_L1_BITS_REMAINING; - pFlac->cache = 0; + bs->nextL2Line = DRFLAC_CACHE_L2_LINE_COUNT(bs); /* <-- This clears the L2 cache. */ + bs->consumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs); /* <-- This clears the L1 cache. */ + bs->cache = 0; + bs->unalignedByteCount = 0; /* <-- This clears the trailing unaligned bytes. */ + bs->unalignedCache = 0; - size_t wholeBytesRemaining = bitsToSeek/8; - if (wholeBytesRemaining > 0) - { - // The next bytes to seek will be located in the L2 cache. The problem is that the L2 cache is not byte aligned, - // but rather DRFLAC_CACHE_L1_SIZE_BYTES aligned (usually 4 or 8). If, for example, the number of bytes to seek is - // 3, we'll need to handle it in a special way. - size_t wholeCacheLinesRemaining = wholeBytesRemaining / DRFLAC_CACHE_L1_SIZE_BYTES; - if (wholeCacheLinesRemaining < DRFLAC_CACHE_L2_LINES_REMAINING) - { - wholeBytesRemaining -= wholeCacheLinesRemaining * DRFLAC_CACHE_L1_SIZE_BYTES; - bitsToSeek -= wholeCacheLinesRemaining * DRFLAC_CACHE_L1_SIZE_BITS; - pFlac->nextL2Line += wholeCacheLinesRemaining; - } - else - { - wholeBytesRemaining -= DRFLAC_CACHE_L2_LINES_REMAINING * DRFLAC_CACHE_L1_SIZE_BYTES; - bitsToSeek -= DRFLAC_CACHE_L2_LINES_REMAINING * DRFLAC_CACHE_L1_SIZE_BITS; - pFlac->nextL2Line += DRFLAC_CACHE_L2_LINES_REMAINING; - - pFlac->onSeek(pFlac->pUserData, (int)wholeBytesRemaining); - pFlac->currentBytePos += wholeBytesRemaining; - bitsToSeek -= wholeBytesRemaining*8; - } - } - - - if (bitsToSeek > 0) { - if (!drflac__reload_cache(pFlac)) { - return false; - } - - return drflac__seek_bits(pFlac, bitsToSeek); - } - - return true; - } +#ifndef DR_FLAC_NO_CRC + bs->crc16Cache = 0; + bs->crc16CacheIgnoredBytes = 0; +#endif } -static bool drflac__read_uint32(drflac* pFlac, unsigned int bitCount, uint32_t* pResultOut) -{ - assert(pFlac != NULL); - assert(pResultOut != NULL); - assert(bitCount > 0); - assert(bitCount <= 32); - if (pFlac->consumedBits == DRFLAC_CACHE_L1_SIZE_BITS) { - if (!drflac__reload_cache(pFlac)) { - return false; +static DRFLAC_INLINE drflac_bool32 drflac__read_uint32(drflac_bs* bs, unsigned int bitCount, drflac_uint32* pResultOut) +{ + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResultOut != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 32); + + if (bs->consumedBits == DRFLAC_CACHE_L1_SIZE_BITS(bs)) { + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; } } - if (bitCount <= DRFLAC_CACHE_L1_BITS_REMAINING) { - if (bitCount < DRFLAC_CACHE_L1_SIZE_BITS) { - *pResultOut = DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bitCount); - pFlac->consumedBits += bitCount; - pFlac->cache <<= bitCount; + if (bitCount <= DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* + If we want to load all 32-bits from a 32-bit cache we need to do it slightly differently because we can't do + a 32-bit shift on a 32-bit integer. This will never be the case on 64-bit caches, so we can have a slightly + more optimal solution for this. + */ +#ifdef DRFLAC_64BIT + *pResultOut = (drflac_uint32)DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, bitCount); + bs->consumedBits += bitCount; + bs->cache <<= bitCount; +#else + if (bitCount < DRFLAC_CACHE_L1_SIZE_BITS(bs)) { + *pResultOut = (drflac_uint32)DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, bitCount); + bs->consumedBits += bitCount; + bs->cache <<= bitCount; } else { - *pResultOut = (uint32_t)pFlac->cache; - pFlac->consumedBits = DRFLAC_CACHE_L1_SIZE_BITS; - pFlac->cache = 0; + /* Cannot shift by 32-bits, so need to do it differently. */ + *pResultOut = (drflac_uint32)bs->cache; + bs->consumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs); + bs->cache = 0; } - return true; +#endif + + return DRFLAC_TRUE; } else { - // It straddles the cached data. It will never cover more than the next chunk. We just read the number in two parts and combine them. - size_t bitCountHi = DRFLAC_CACHE_L1_BITS_REMAINING; - size_t bitCountLo = bitCount - bitCountHi; - uint32_t resultHi = DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bitCountHi); + /* It straddles the cached data. It will never cover more than the next chunk. We just read the number in two parts and combine them. */ + drflac_uint32 bitCountHi = DRFLAC_CACHE_L1_BITS_REMAINING(bs); + drflac_uint32 bitCountLo = bitCount - bitCountHi; + drflac_uint32 resultHi; - if (!drflac__reload_cache(pFlac)) { - return false; + DRFLAC_ASSERT(bitCountHi > 0); + DRFLAC_ASSERT(bitCountHi < 32); + resultHi = (drflac_uint32)DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, bitCountHi); + + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + if (bitCountLo > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; } - *pResultOut = (resultHi << bitCountLo) | DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bitCountLo); - pFlac->consumedBits += bitCountLo; - pFlac->cache <<= bitCountLo; - return true; + *pResultOut = (resultHi << bitCountLo) | (drflac_uint32)DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, bitCountLo); + bs->consumedBits += bitCountLo; + bs->cache <<= bitCountLo; + return DRFLAC_TRUE; } } -static bool drflac__read_int32(drflac* pFlac, unsigned int bitCount, int32_t* pResult) +static drflac_bool32 drflac__read_int32(drflac_bs* bs, unsigned int bitCount, drflac_int32* pResult) { - assert(pFlac != NULL); - assert(pResult != NULL); - assert(bitCount > 0); - assert(bitCount <= 32); + drflac_uint32 result; - uint32_t result; - if (!drflac__read_uint32(pFlac, bitCount, &result)) { - return false; + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 32); + + if (!drflac__read_uint32(bs, bitCount, &result)) { + return DRFLAC_FALSE; } - if ((result & (1 << (bitCount - 1)))) { // TODO: See if we can get rid of this branch. - result |= (-1 << bitCount); + /* Do not attempt to shift by 32 as it's undefined. */ + if (bitCount < 32) { + drflac_uint32 signbit; + signbit = ((result >> (bitCount-1)) & 0x01); + result |= (~signbit + 1) << bitCount; } - *pResult = (int32_t)result; - return true; + *pResult = (drflac_int32)result; + return DRFLAC_TRUE; } -static bool drflac__read_uint64(drflac* pFlac, unsigned int bitCount, uint64_t* pResultOut) +#ifdef DRFLAC_64BIT +static drflac_bool32 drflac__read_uint64(drflac_bs* bs, unsigned int bitCount, drflac_uint64* pResultOut) { - assert(bitCount <= 64); - assert(bitCount > 32); + drflac_uint32 resultHi; + drflac_uint32 resultLo; - uint32_t resultHi; - if (!drflac__read_uint32(pFlac, bitCount - 32, &resultHi)) { - return false; + DRFLAC_ASSERT(bitCount <= 64); + DRFLAC_ASSERT(bitCount > 32); + + if (!drflac__read_uint32(bs, bitCount - 32, &resultHi)) { + return DRFLAC_FALSE; } - uint32_t resultLo; - if (!drflac__read_uint32(pFlac, 32, &resultLo)) { - return false; + if (!drflac__read_uint32(bs, 32, &resultLo)) { + return DRFLAC_FALSE; } - *pResultOut = (((uint64_t)resultHi) << 32) | ((uint64_t)resultLo); - return true; + *pResultOut = (((drflac_uint64)resultHi) << 32) | ((drflac_uint64)resultLo); + return DRFLAC_TRUE; +} +#endif + +/* Function below is unused, but leaving it here in case I need to quickly add it again. */ +#if 0 +static drflac_bool32 drflac__read_int64(drflac_bs* bs, unsigned int bitCount, drflac_int64* pResultOut) +{ + drflac_uint64 result; + drflac_uint64 signbit; + + DRFLAC_ASSERT(bitCount <= 64); + + if (!drflac__read_uint64(bs, bitCount, &result)) { + return DRFLAC_FALSE; + } + + signbit = ((result >> (bitCount-1)) & 0x01); + result |= (~signbit + 1) << bitCount; + + *pResultOut = (drflac_int64)result; + return DRFLAC_TRUE; +} +#endif + +static drflac_bool32 drflac__read_uint16(drflac_bs* bs, unsigned int bitCount, drflac_uint16* pResult) +{ + drflac_uint32 result; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 16); + + if (!drflac__read_uint32(bs, bitCount, &result)) { + return DRFLAC_FALSE; + } + + *pResult = (drflac_uint16)result; + return DRFLAC_TRUE; } -static bool drflac__read_int64(drflac* pFlac, unsigned int bitCount, int64_t* pResultOut) +#if 0 +static drflac_bool32 drflac__read_int16(drflac_bs* bs, unsigned int bitCount, drflac_int16* pResult) { - assert(bitCount <= 64); + drflac_int32 result; - uint64_t result; - if (!drflac__read_uint64(pFlac, bitCount, &result)) { - return false; + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 16); + + if (!drflac__read_int32(bs, bitCount, &result)) { + return DRFLAC_FALSE; } - if ((result & (1ULL << (bitCount - 1)))) { // TODO: See if we can get rid of this branch. - result |= (-1LL << bitCount); + *pResult = (drflac_int16)result; + return DRFLAC_TRUE; +} +#endif + +static drflac_bool32 drflac__read_uint8(drflac_bs* bs, unsigned int bitCount, drflac_uint8* pResult) +{ + drflac_uint32 result; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 8); + + if (!drflac__read_uint32(bs, bitCount, &result)) { + return DRFLAC_FALSE; } - *pResultOut = (int64_t)result; - return true; + *pResult = (drflac_uint8)result; + return DRFLAC_TRUE; } -static bool drflac__read_uint16(drflac* pFlac, unsigned int bitCount, uint16_t* pResult) +static drflac_bool32 drflac__read_int8(drflac_bs* bs, unsigned int bitCount, drflac_int8* pResult) { - assert(pFlac != NULL); - assert(pResult != NULL); - assert(bitCount > 0); - assert(bitCount <= 16); + drflac_int32 result; - uint32_t result; - if (!drflac__read_uint32(pFlac, bitCount, &result)) { - return false; + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pResult != NULL); + DRFLAC_ASSERT(bitCount > 0); + DRFLAC_ASSERT(bitCount <= 8); + + if (!drflac__read_int32(bs, bitCount, &result)) { + return DRFLAC_FALSE; } - *pResult = (uint16_t)result; - return true; -} - -static bool drflac__read_int16(drflac* pFlac, unsigned int bitCount, int16_t* pResult) -{ - assert(pFlac != NULL); - assert(pResult != NULL); - assert(bitCount > 0); - assert(bitCount <= 16); - - int32_t result; - if (!drflac__read_int32(pFlac, bitCount, &result)) { - return false; - } - - *pResult = (int16_t)result; - return true; -} - -static bool drflac__read_uint8(drflac* pFlac, unsigned int bitCount, uint8_t* pResult) -{ - assert(pFlac != NULL); - assert(pResult != NULL); - assert(bitCount > 0); - assert(bitCount <= 8); - - uint32_t result; - if (!drflac__read_uint32(pFlac, bitCount, &result)) { - return false; - } - - *pResult = (uint8_t)result; - return true; -} - -static bool drflac__read_int8(drflac* pFlac, unsigned int bitCount, int8_t* pResult) -{ - assert(pFlac != NULL); - assert(pResult != NULL); - assert(bitCount > 0); - assert(bitCount <= 8); - - int32_t result; - if (!drflac__read_int32(pFlac, bitCount, &result)) { - return false; - } - - *pResult = (int8_t)result; - return true; + *pResult = (drflac_int8)result; + return DRFLAC_TRUE; } -static inline bool drflac__seek_past_next_set_bit(drflac* pFlac, unsigned int* pOffsetOut) +static drflac_bool32 drflac__seek_bits(drflac_bs* bs, size_t bitsToSeek) { - unsigned int zeroCounter = 0; - while (pFlac->cache == 0) { - zeroCounter += (unsigned int)DRFLAC_CACHE_L1_BITS_REMAINING; - if (!drflac__reload_cache(pFlac)) { - return false; + if (bitsToSeek <= DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + bs->consumedBits += (drflac_uint32)bitsToSeek; + bs->cache <<= bitsToSeek; + return DRFLAC_TRUE; + } else { + /* It straddles the cached data. This function isn't called too frequently so I'm favouring simplicity here. */ + bitsToSeek -= DRFLAC_CACHE_L1_BITS_REMAINING(bs); + bs->consumedBits += DRFLAC_CACHE_L1_BITS_REMAINING(bs); + bs->cache = 0; + + /* Simple case. Seek in groups of the same number as bits that fit within a cache line. */ +#ifdef DRFLAC_64BIT + while (bitsToSeek >= DRFLAC_CACHE_L1_SIZE_BITS(bs)) { + drflac_uint64 bin; + if (!drflac__read_uint64(bs, DRFLAC_CACHE_L1_SIZE_BITS(bs), &bin)) { + return DRFLAC_FALSE; + } + bitsToSeek -= DRFLAC_CACHE_L1_SIZE_BITS(bs); + } +#else + while (bitsToSeek >= DRFLAC_CACHE_L1_SIZE_BITS(bs)) { + drflac_uint32 bin; + if (!drflac__read_uint32(bs, DRFLAC_CACHE_L1_SIZE_BITS(bs), &bin)) { + return DRFLAC_FALSE; + } + bitsToSeek -= DRFLAC_CACHE_L1_SIZE_BITS(bs); + } +#endif + + /* Whole leftover bytes. */ + while (bitsToSeek >= 8) { + drflac_uint8 bin; + if (!drflac__read_uint8(bs, 8, &bin)) { + return DRFLAC_FALSE; + } + bitsToSeek -= 8; + } + + /* Leftover bits. */ + if (bitsToSeek > 0) { + drflac_uint8 bin; + if (!drflac__read_uint8(bs, (drflac_uint32)bitsToSeek, &bin)) { + return DRFLAC_FALSE; + } + bitsToSeek = 0; /* <-- Necessary for the assert below. */ + } + + DRFLAC_ASSERT(bitsToSeek == 0); + return DRFLAC_TRUE; + } +} + + +/* This function moves the bit streamer to the first bit after the sync code (bit 15 of the of the frame header). It will also update the CRC-16. */ +static drflac_bool32 drflac__find_and_seek_to_next_sync_code(drflac_bs* bs) +{ + DRFLAC_ASSERT(bs != NULL); + + /* + The sync code is always aligned to 8 bits. This is convenient for us because it means we can do byte-aligned movements. The first + thing to do is align to the next byte. + */ + if (!drflac__seek_bits(bs, DRFLAC_CACHE_L1_BITS_REMAINING(bs) & 7)) { + return DRFLAC_FALSE; + } + + for (;;) { + drflac_uint8 hi; + +#ifndef DR_FLAC_NO_CRC + drflac__reset_crc16(bs); +#endif + + if (!drflac__read_uint8(bs, 8, &hi)) { + return DRFLAC_FALSE; + } + + if (hi == 0xFF) { + drflac_uint8 lo; + if (!drflac__read_uint8(bs, 6, &lo)) { + return DRFLAC_FALSE; + } + + if (lo == 0x3E) { + return DRFLAC_TRUE; + } else { + if (!drflac__seek_bits(bs, DRFLAC_CACHE_L1_BITS_REMAINING(bs) & 7)) { + return DRFLAC_FALSE; + } + } } } - // At this point the cache should not be zero, in which case we know the first set bit should be somewhere in here. There is - // no need for us to perform any cache reloading logic here which should make things much faster. - assert(pFlac->cache != 0); + /* Should never get here. */ + /*return DRFLAC_FALSE;*/ +} - unsigned int bitOffsetTable[] = { + +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) +#define DRFLAC_IMPLEMENT_CLZ_LZCNT +#endif +#if defined(_MSC_VER) && _MSC_VER >= 1400 && (defined(DRFLAC_X64) || defined(DRFLAC_X86)) && !defined(__clang__) +#define DRFLAC_IMPLEMENT_CLZ_MSVC +#endif +#if defined(__WATCOMC__) && defined(__386__) +#define DRFLAC_IMPLEMENT_CLZ_WATCOM +#endif +#ifdef __MRC__ +#include +#define DRFLAC_IMPLEMENT_CLZ_MRC +#endif + +static DRFLAC_INLINE drflac_uint32 drflac__clz_software(drflac_cache_t x) +{ + drflac_uint32 n; + static drflac_uint32 clz_table_4[] = { 0, 4, 3, 3, @@ -915,106 +2655,293 @@ static inline bool drflac__seek_past_next_set_bit(drflac* pFlac, unsigned int* p 1, 1, 1, 1, 1, 1, 1, 1 }; - unsigned int setBitOffsetPlus1 = bitOffsetTable[DRFLAC_CACHE_L1_SELECT_AND_SHIFT(4)]; - if (setBitOffsetPlus1 == 0) { - if (pFlac->cache == 1) { - setBitOffsetPlus1 = DRFLAC_CACHE_L1_SIZE_BITS; - } else { - setBitOffsetPlus1 = 5; - for (;;) - { - if ((pFlac->cache & DRFLAC_CACHE_L1_SELECT(setBitOffsetPlus1))) { - break; - } + if (x == 0) { + return sizeof(x)*8; + } - setBitOffsetPlus1 += 1; + n = clz_table_4[x >> (sizeof(x)*8 - 4)]; + if (n == 0) { +#ifdef DRFLAC_64BIT + if ((x & ((drflac_uint64)0xFFFFFFFF << 32)) == 0) { n = 32; x <<= 32; } + if ((x & ((drflac_uint64)0xFFFF0000 << 32)) == 0) { n += 16; x <<= 16; } + if ((x & ((drflac_uint64)0xFF000000 << 32)) == 0) { n += 8; x <<= 8; } + if ((x & ((drflac_uint64)0xF0000000 << 32)) == 0) { n += 4; x <<= 4; } +#else + if ((x & 0xFFFF0000) == 0) { n = 16; x <<= 16; } + if ((x & 0xFF000000) == 0) { n += 8; x <<= 8; } + if ((x & 0xF0000000) == 0) { n += 4; x <<= 4; } +#endif + n += clz_table_4[x >> (sizeof(x)*8 - 4)]; + } + + return n - 1; +} + +#ifdef DRFLAC_IMPLEMENT_CLZ_LZCNT +static DRFLAC_INLINE drflac_bool32 drflac__is_lzcnt_supported(void) +{ + /* Fast compile time check for ARM. */ +#if defined(DRFLAC_HAS_LZCNT_INTRINSIC) && defined(DRFLAC_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 5) + return DRFLAC_TRUE; +#elif defined(__MRC__) + return DRFLAC_TRUE; +#else + /* If the compiler itself does not support the intrinsic then we'll need to return false. */ + #ifdef DRFLAC_HAS_LZCNT_INTRINSIC + return drflac__gIsLZCNTSupported; + #else + return DRFLAC_FALSE; + #endif +#endif +} + +static DRFLAC_INLINE drflac_uint32 drflac__clz_lzcnt(drflac_cache_t x) +{ + /* + It's critical for competitive decoding performance that this function be highly optimal. With MSVC we can use the __lzcnt64() and __lzcnt() intrinsics + to achieve good performance, however on GCC and Clang it's a little bit more annoying. The __builtin_clzl() and __builtin_clzll() intrinsics leave + it undefined as to the return value when `x` is 0. We need this to be well defined as returning 32 or 64, depending on whether or not it's a 32- or + 64-bit build. To work around this we would need to add a conditional to check for the x = 0 case, but this creates unnecessary inefficiency. To work + around this problem I have written some inline assembly to emit the LZCNT (x86) or CLZ (ARM) instruction directly which removes the need to include + the conditional. This has worked well in the past, but for some reason Clang's MSVC compatible driver, clang-cl, does not seem to be handling this + in the same way as the normal Clang driver. It seems that `clang-cl` is just outputting the wrong results sometimes, maybe due to some register + getting clobbered? + + I'm not sure if this is a bug with dr_flac's inlined assembly (most likely), a bug in `clang-cl` or just a misunderstanding on my part with inline + assembly rules for `clang-cl`. If somebody can identify an error in dr_flac's inlined assembly I'm happy to get that fixed. + + Fortunately there is an easy workaround for this. Clang implements MSVC-specific intrinsics for compatibility. It also defines _MSC_VER for extra + compatibility. We can therefore just check for _MSC_VER and use the MSVC intrinsic which, fortunately for us, Clang supports. It would still be nice + to know how to fix the inlined assembly for correctness sake, however. + */ + +#if defined(_MSC_VER) /*&& !defined(__clang__)*/ /* <-- Intentionally wanting Clang to use the MSVC __lzcnt64/__lzcnt intrinsics due to above ^. */ + #ifdef DRFLAC_64BIT + return (drflac_uint32)__lzcnt64(x); + #else + return (drflac_uint32)__lzcnt(x); + #endif +#else + #if defined(__GNUC__) || defined(__clang__) + #if defined(DRFLAC_X64) + { + drflac_uint64 r; + __asm__ __volatile__ ( + "lzcnt{ %1, %0| %0, %1}" : "=r"(r) : "r"(x) : "cc" + ); + + return (drflac_uint32)r; } + #elif defined(DRFLAC_X86) + { + drflac_uint32 r; + __asm__ __volatile__ ( + "lzcnt{l %1, %0| %0, %1}" : "=r"(r) : "r"(x) : "cc" + ); + + return r; + } + #elif defined(DRFLAC_ARM) && (defined(__ARM_ARCH) && __ARM_ARCH >= 5) && !defined(__ARM_ARCH_6M__) && !(defined(__thumb__) && !defined(__thumb2__)) && !defined(DRFLAC_64BIT) /* <-- I haven't tested 64-bit inline assembly, so only enabling this for the 32-bit build for now. */ + { + unsigned int r; + __asm__ __volatile__ ( + #if defined(DRFLAC_64BIT) + "clz %w[out], %w[in]" : [out]"=r"(r) : [in]"r"(x) /* <-- This is untested. If someone in the community could test this, that would be appreciated! */ + #else + "clz %[out], %[in]" : [out]"=r"(r) : [in]"r"(x) + #endif + ); + + return r; + } + #else + if (x == 0) { + return sizeof(x)*8; + } + #ifdef DRFLAC_64BIT + return (drflac_uint32)__builtin_clzll((drflac_uint64)x); + #else + return (drflac_uint32)__builtin_clzl((drflac_uint32)x); + #endif + #endif + #else + /* Unsupported compiler. */ + #error "This compiler does not support the lzcnt intrinsic." + #endif +#endif +} +#endif + +#ifdef DRFLAC_IMPLEMENT_CLZ_MSVC +#include /* For BitScanReverse(). */ + +static DRFLAC_INLINE drflac_uint32 drflac__clz_msvc(drflac_cache_t x) +{ + drflac_uint32 n; + + if (x == 0) { + return sizeof(x)*8; + } + +#ifdef DRFLAC_64BIT + _BitScanReverse64((unsigned long*)&n, x); +#else + _BitScanReverse((unsigned long*)&n, x); +#endif + return sizeof(x)*8 - n - 1; +} +#endif + +#ifdef DRFLAC_IMPLEMENT_CLZ_WATCOM +static __inline drflac_uint32 drflac__clz_watcom (drflac_uint32); +#ifdef DRFLAC_IMPLEMENT_CLZ_WATCOM_LZCNT +/* Use the LZCNT instruction (only available on some processors since the 2010s). */ +#pragma aux drflac__clz_watcom_lzcnt = \ + "db 0F3h, 0Fh, 0BDh, 0C0h" /* lzcnt eax, eax */ \ + parm [eax] \ + value [eax] \ + modify nomemory; +#else +/* Use the 386+-compatible implementation. */ +#pragma aux drflac__clz_watcom = \ + "bsr eax, eax" \ + "xor eax, 31" \ + parm [eax] nomemory \ + value [eax] \ + modify exact [eax] nomemory; +#endif +#endif + +static DRFLAC_INLINE drflac_uint32 drflac__clz(drflac_cache_t x) +{ +#ifdef DRFLAC_IMPLEMENT_CLZ_LZCNT + if (drflac__is_lzcnt_supported()) { + return drflac__clz_lzcnt(x); + } else +#endif + { +#ifdef DRFLAC_IMPLEMENT_CLZ_MSVC + return drflac__clz_msvc(x); +#elif defined(DRFLAC_IMPLEMENT_CLZ_WATCOM_LZCNT) + return drflac__clz_watcom_lzcnt(x); +#elif defined(DRFLAC_IMPLEMENT_CLZ_WATCOM) + return (x == 0) ? sizeof(x)*8 : drflac__clz_watcom(x); +#elif defined(__MRC__) + return __cntlzw(x); +#else + return drflac__clz_software(x); +#endif + } +} + + +static DRFLAC_INLINE drflac_bool32 drflac__seek_past_next_set_bit(drflac_bs* bs, unsigned int* pOffsetOut) +{ + drflac_uint32 zeroCounter = 0; + drflac_uint32 setBitOffsetPlus1; + + while (bs->cache == 0) { + zeroCounter += (drflac_uint32)DRFLAC_CACHE_L1_BITS_REMAINING(bs); + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; } } - pFlac->consumedBits += setBitOffsetPlus1; - pFlac->cache <<= setBitOffsetPlus1; + if (bs->cache == 1) { + /* Not catching this would lead to undefined behaviour: a shift of a 32-bit number by 32 or more is undefined */ + *pOffsetOut = zeroCounter + (drflac_uint32)DRFLAC_CACHE_L1_BITS_REMAINING(bs) - 1; + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + + return DRFLAC_TRUE; + } + + setBitOffsetPlus1 = drflac__clz(bs->cache); + setBitOffsetPlus1 += 1; + + if (setBitOffsetPlus1 > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; + } + + bs->consumedBits += setBitOffsetPlus1; + bs->cache <<= setBitOffsetPlus1; *pOffsetOut = zeroCounter + setBitOffsetPlus1 - 1; - return true; + return DRFLAC_TRUE; } -static bool drflac__seek_to_byte(drflac* pFlac, long long offsetFromStart) +static drflac_bool32 drflac__seek_to_byte(drflac_bs* bs, drflac_uint64 offsetFromStart) { - assert(pFlac != NULL); + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(offsetFromStart > 0); - long long bytesToMove = offsetFromStart - pFlac->currentBytePos; - if (bytesToMove == 0) { - return 1; - } + /* + Seeking from the start is not quite as trivial as it sounds because the onSeek callback takes a signed 32-bit integer (which + is intentional because it simplifies the implementation of the onSeek callbacks), however offsetFromStart is unsigned 64-bit. + To resolve we just need to do an initial seek from the start, and then a series of offset seeks to make up the remainder. + */ + if (offsetFromStart > 0x7FFFFFFF) { + drflac_uint64 bytesRemaining = offsetFromStart; + if (!bs->onSeek(bs->pUserData, 0x7FFFFFFF, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + bytesRemaining -= 0x7FFFFFFF; - if (bytesToMove > 0x7FFFFFFF) { - while (bytesToMove > 0x7FFFFFFF) { - if (!pFlac->onSeek(pFlac->pUserData, 0x7FFFFFFF)) { - return 0; + while (bytesRemaining > 0x7FFFFFFF) { + if (!bs->onSeek(bs->pUserData, 0x7FFFFFFF, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; } + bytesRemaining -= 0x7FFFFFFF; + } - pFlac->currentBytePos += 0x7FFFFFFF; - bytesToMove -= 0x7FFFFFFF; + if (bytesRemaining > 0) { + if (!bs->onSeek(bs->pUserData, (int)bytesRemaining, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } } } else { - while (bytesToMove < (int)0x80000000) { - if (!pFlac->onSeek(pFlac->pUserData, (int)0x80000000)) { - return 0; - } - - pFlac->currentBytePos += (int)0x80000000; - bytesToMove -= (int)0x80000000; + if (!bs->onSeek(bs->pUserData, (int)offsetFromStart, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; } } - assert(bytesToMove <= 0x7FFFFFFF && bytesToMove >= (int)0x80000000); - - bool result = pFlac->onSeek(pFlac->pUserData, (int)bytesToMove); // <-- Safe cast as per the assert above. - pFlac->currentBytePos += (int)bytesToMove; - - pFlac->consumedBits = DRFLAC_CACHE_L1_SIZE_BITS; - pFlac->cache = 0; - pFlac->nextL2Line = DRFLAC_CACHE_L2_LINE_COUNT; // <-- This clears the L2 cache. - - return result; -} - -static long long drflac__tell(drflac* pFlac) -{ - assert(pFlac != NULL); - - size_t unreadBytesFromL1 = (DRFLAC_CACHE_L1_SIZE_BYTES - (pFlac->consumedBits/8)); - size_t unreadBytesFromL2 = (DRFLAC_CACHE_L2_SIZE_BYTES - ((pFlac->nextL2Line - pFlac->unusedL2Lines)*DRFLAC_CACHE_L1_SIZE_BYTES)); - - return pFlac->currentBytePos - unreadBytesFromL1 - unreadBytesFromL2; + /* The cache should be reset to force a reload of fresh data from the client. */ + drflac__reset_cache(bs); + return DRFLAC_TRUE; } - -static bool drflac__read_utf8_coded_number(drflac* pFlac, unsigned long long* pNumberOut) +static drflac_result drflac__read_utf8_coded_number(drflac_bs* bs, drflac_uint64* pNumberOut, drflac_uint8* pCRCOut) { - assert(pFlac != NULL); - assert(pNumberOut != NULL); + drflac_uint8 crc; + drflac_uint64 result; + drflac_uint8 utf8[7] = {0}; + int byteCount; + int i; - // We should never need to read UTF-8 data while not being aligned to a byte boundary. Therefore we can grab the data - // directly from the input stream rather than using drflac__read_uint8(). - assert((pFlac->consumedBits & 7) == 0); + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pNumberOut != NULL); + DRFLAC_ASSERT(pCRCOut != NULL); - unsigned char utf8[7] = {0}; - if (!drflac__read_uint8(pFlac, 8, utf8)) { + crc = *pCRCOut; + + if (!drflac__read_uint8(bs, 8, utf8)) { *pNumberOut = 0; - return false; + return DRFLAC_AT_END; } + crc = drflac_crc8(crc, utf8[0], 8); if ((utf8[0] & 0x80) == 0) { *pNumberOut = utf8[0]; - return true; + *pCRCOut = crc; + return DRFLAC_SUCCESS; } - int byteCount = 1; + /*byteCount = 1;*/ if ((utf8[0] & 0xE0) == 0xC0) { byteCount = 2; } else if ((utf8[0] & 0xF0) == 0xE0) { @@ -1029,212 +2956,67 @@ static bool drflac__read_utf8_coded_number(drflac* pFlac, unsigned long long* pN byteCount = 7; } else { *pNumberOut = 0; - return false; // Bad UTF-8 encoding. + return DRFLAC_CRC_MISMATCH; /* Bad UTF-8 encoding. */ } - // Read extra bytes. - assert(byteCount > 1); + /* Read extra bytes. */ + DRFLAC_ASSERT(byteCount > 1); - unsigned long long result = ((long long)(utf8[0] & (0xFF >> (byteCount + 1)))); - for (int i = 1; i < byteCount; ++i) { - if (!drflac__read_uint8(pFlac, 8, utf8 + i)) { + result = (drflac_uint64)(utf8[0] & (0xFF >> (byteCount + 1))); + for (i = 1; i < byteCount; ++i) { + if (!drflac__read_uint8(bs, 8, utf8 + i)) { *pNumberOut = 0; - return false; + return DRFLAC_AT_END; } + crc = drflac_crc8(crc, utf8[i], 8); result = (result << 6) | (utf8[i] & 0x3F); } *pNumberOut = result; - return true; + *pCRCOut = crc; + return DRFLAC_SUCCESS; } - -static DRFLAC_INLINE bool drflac__read_and_seek_rice(drflac* pFlac, unsigned char m) +static DRFLAC_INLINE drflac_uint32 drflac__ilog2_u32(drflac_uint32 x) { - unsigned int unused; - if (!drflac__seek_past_next_set_bit(pFlac, &unused)) { - return false; +#if 1 /* Needs optimizing. */ + drflac_uint32 result = 0; + while (x > 0) { + result += 1; + x >>= 1; } - if (m > 0) { - if (!drflac__seek_bits(pFlac, m)) { - return false; - } - } - - return true; -} - - -// The next two functions are responsible for calculating the prediction. -// -// When the bits per sample is >16 we need to use 64-bit integer arithmetic because otherwise we'll run out of precision. It's -// safe to assume this will be slower on 32-bit platforms so we use a more optimal solution when the bits per sample is <=16. -// -// -// Optimization Experiment #1 -// -// The first optimization experiment I'm trying here is a loop unroll for the most common LPC orders. I've done a little test -// and the results are as follows, in order of most common: -// 1) order = 8 : 93.1M -// 2) order = 7 : 36.6M -// 3) order = 3 : 33.2M -// 4) order = 6 : 20.9M -// 5) order = 5 : 18.1M -// 6) order = 4 : 15.8M -// 7) order = 12 : 10.8M -// 8) order = 2 : 9.8M -// 9) order = 1 : 1.6M -// 10) order = 10 : 1.0M -// 11) order = 9 : 0.8M -// 12) order = 11 : 0.8M -// -// We'll experiment with unrolling the top 8 most common ones. We'll ignore the least common ones since there seems to be a -// large drop off there. -// -// Result: There's a tiny improvement in some cases, but it could just be within margin of error so unsure if it's worthwhile -// just yet. -static DRFLAC_INLINE int32_t drflac__calculate_prediction_32(unsigned int order, int shift, const short* coefficients, int32_t* pDecodedSamples) -{ - assert(order <= 32); - - // 32-bit version. - - // This method is slower on both 32- and 64-bit builds with VC++. Leaving this here for now just in case we need it later - // for whatever reason. -#if 0 - int prediction; - if (order == 8) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - prediction += coefficients[3] * pDecodedSamples[-4]; - prediction += coefficients[4] * pDecodedSamples[-5]; - prediction += coefficients[5] * pDecodedSamples[-6]; - prediction += coefficients[6] * pDecodedSamples[-7]; - prediction += coefficients[7] * pDecodedSamples[-8]; - } - else if (order == 7) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - prediction += coefficients[3] * pDecodedSamples[-4]; - prediction += coefficients[4] * pDecodedSamples[-5]; - prediction += coefficients[5] * pDecodedSamples[-6]; - prediction += coefficients[6] * pDecodedSamples[-7]; - } - else if (order == 3) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - } - else if (order == 6) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - prediction += coefficients[3] * pDecodedSamples[-4]; - prediction += coefficients[4] * pDecodedSamples[-5]; - prediction += coefficients[5] * pDecodedSamples[-6]; - } - else if (order == 5) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - prediction += coefficients[3] * pDecodedSamples[-4]; - prediction += coefficients[4] * pDecodedSamples[-5]; - } - else if (order == 4) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - prediction += coefficients[3] * pDecodedSamples[-4]; - } - else if (order == 12) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - prediction += coefficients[3] * pDecodedSamples[-4]; - prediction += coefficients[4] * pDecodedSamples[-5]; - prediction += coefficients[5] * pDecodedSamples[-6]; - prediction += coefficients[6] * pDecodedSamples[-7]; - prediction += coefficients[7] * pDecodedSamples[-8]; - prediction += coefficients[8] * pDecodedSamples[-9]; - prediction += coefficients[9] * pDecodedSamples[-10]; - prediction += coefficients[10] * pDecodedSamples[-11]; - prediction += coefficients[11] * pDecodedSamples[-12]; - } - else if (order == 2) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - } - else if (order == 1) - { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - } - else if (order == 10) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - prediction += coefficients[3] * pDecodedSamples[-4]; - prediction += coefficients[4] * pDecodedSamples[-5]; - prediction += coefficients[5] * pDecodedSamples[-6]; - prediction += coefficients[6] * pDecodedSamples[-7]; - prediction += coefficients[7] * pDecodedSamples[-8]; - prediction += coefficients[8] * pDecodedSamples[-9]; - prediction += coefficients[9] * pDecodedSamples[-10]; - } - else if (order == 9) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - prediction += coefficients[3] * pDecodedSamples[-4]; - prediction += coefficients[4] * pDecodedSamples[-5]; - prediction += coefficients[5] * pDecodedSamples[-6]; - prediction += coefficients[6] * pDecodedSamples[-7]; - prediction += coefficients[7] * pDecodedSamples[-8]; - prediction += coefficients[8] * pDecodedSamples[-9]; - } - else if (order == 11) - { - prediction = coefficients[0] * pDecodedSamples[-1]; - prediction += coefficients[1] * pDecodedSamples[-2]; - prediction += coefficients[2] * pDecodedSamples[-3]; - prediction += coefficients[3] * pDecodedSamples[-4]; - prediction += coefficients[4] * pDecodedSamples[-5]; - prediction += coefficients[5] * pDecodedSamples[-6]; - prediction += coefficients[6] * pDecodedSamples[-7]; - prediction += coefficients[7] * pDecodedSamples[-8]; - prediction += coefficients[8] * pDecodedSamples[-9]; - prediction += coefficients[9] * pDecodedSamples[-10]; - prediction += coefficients[10] * pDecodedSamples[-11]; - } - else - { - prediction = 0; - for (int j = 0; j < (int)order; ++j) { - prediction += coefficients[j] * pDecodedSamples[-j-1]; - } - } + return result; #endif +} - // Experiment #2. See if we can use a switch and let the compiler optimize it to a jump table. - // Result: VC++ definitely optimizes this to a single jmp as expected. I expect other compilers should do the same, but I've - // not verified yet. -#if 1 - int prediction = 0; +static DRFLAC_INLINE drflac_bool32 drflac__use_64_bit_prediction(drflac_uint32 bitsPerSample, drflac_uint32 order, drflac_uint32 precision) +{ + /* https://web.archive.org/web/20220205005724/https://github.com/ietf-wg-cellar/flac-specification/blob/37a49aa48ba4ba12e8757badfc59c0df35435fec/rfc_backmatter.md */ + return bitsPerSample + precision + drflac__ilog2_u32(order) > 32; +} + +/* +The next two functions are responsible for calculating the prediction. + +When the bits per sample is >16 we need to use 64-bit integer arithmetic because otherwise we'll run out of precision. It's +safe to assume this will be slower on 32-bit platforms so we use a more optimal solution when the bits per sample is <=16. +*/ +#if defined(__clang__) +__attribute__((no_sanitize("signed-integer-overflow"))) +#endif +static DRFLAC_INLINE drflac_int32 drflac__calculate_prediction_32(drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pDecodedSamples) +{ + drflac_int32 prediction = 0; + + DRFLAC_ASSERT(order <= 32); + + /* 32-bit version. */ + + /* VC++ optimizes this to a single jmp. I've not yet verified this for other compilers. */ switch (order) { case 32: prediction += coefficients[31] * pDecodedSamples[-32]; @@ -1270,472 +3052,1942 @@ static DRFLAC_INLINE int32_t drflac__calculate_prediction_32(unsigned int order, case 2: prediction += coefficients[ 1] * pDecodedSamples[- 2]; case 1: prediction += coefficients[ 0] * pDecodedSamples[- 1]; } -#endif - return (int32_t)(prediction >> shift); + return (drflac_int32)(prediction >> shift); } -static DRFLAC_INLINE int32_t drflac__calculate_prediction(unsigned int order, int shift, const short* coefficients, int32_t* pDecodedSamples) +static DRFLAC_INLINE drflac_int32 drflac__calculate_prediction_64(drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pDecodedSamples) { - assert(order <= 32); + drflac_int64 prediction; - // 64-bit version. + DRFLAC_ASSERT(order <= 32); - // This method is faster on the 32-bit build when compiling with VC++. See note below. + /* 64-bit version. */ + + /* This method is faster on the 32-bit build when compiling with VC++. See note below. */ #ifndef DRFLAC_64BIT - long long prediction; if (order == 8) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; - prediction += (long long)coefficients[3] * (long long)pDecodedSamples[-4]; - prediction += (long long)coefficients[4] * (long long)pDecodedSamples[-5]; - prediction += (long long)coefficients[5] * (long long)pDecodedSamples[-6]; - prediction += (long long)coefficients[6] * (long long)pDecodedSamples[-7]; - prediction += (long long)coefficients[7] * (long long)pDecodedSamples[-8]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; } else if (order == 7) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; - prediction += (long long)coefficients[3] * (long long)pDecodedSamples[-4]; - prediction += (long long)coefficients[4] * (long long)pDecodedSamples[-5]; - prediction += (long long)coefficients[5] * (long long)pDecodedSamples[-6]; - prediction += (long long)coefficients[6] * (long long)pDecodedSamples[-7]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; } else if (order == 3) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; } else if (order == 6) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; - prediction += (long long)coefficients[3] * (long long)pDecodedSamples[-4]; - prediction += (long long)coefficients[4] * (long long)pDecodedSamples[-5]; - prediction += (long long)coefficients[5] * (long long)pDecodedSamples[-6]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; } else if (order == 5) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; - prediction += (long long)coefficients[3] * (long long)pDecodedSamples[-4]; - prediction += (long long)coefficients[4] * (long long)pDecodedSamples[-5]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; } else if (order == 4) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; - prediction += (long long)coefficients[3] * (long long)pDecodedSamples[-4]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; } else if (order == 12) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; - prediction += (long long)coefficients[3] * (long long)pDecodedSamples[-4]; - prediction += (long long)coefficients[4] * (long long)pDecodedSamples[-5]; - prediction += (long long)coefficients[5] * (long long)pDecodedSamples[-6]; - prediction += (long long)coefficients[6] * (long long)pDecodedSamples[-7]; - prediction += (long long)coefficients[7] * (long long)pDecodedSamples[-8]; - prediction += (long long)coefficients[8] * (long long)pDecodedSamples[-9]; - prediction += (long long)coefficients[9] * (long long)pDecodedSamples[-10]; - prediction += (long long)coefficients[10] * (long long)pDecodedSamples[-11]; - prediction += (long long)coefficients[11] * (long long)pDecodedSamples[-12]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; + prediction += coefficients[8] * (drflac_int64)pDecodedSamples[-9]; + prediction += coefficients[9] * (drflac_int64)pDecodedSamples[-10]; + prediction += coefficients[10] * (drflac_int64)pDecodedSamples[-11]; + prediction += coefficients[11] * (drflac_int64)pDecodedSamples[-12]; } else if (order == 2) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; } else if (order == 1) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; } else if (order == 10) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; - prediction += (long long)coefficients[3] * (long long)pDecodedSamples[-4]; - prediction += (long long)coefficients[4] * (long long)pDecodedSamples[-5]; - prediction += (long long)coefficients[5] * (long long)pDecodedSamples[-6]; - prediction += (long long)coefficients[6] * (long long)pDecodedSamples[-7]; - prediction += (long long)coefficients[7] * (long long)pDecodedSamples[-8]; - prediction += (long long)coefficients[8] * (long long)pDecodedSamples[-9]; - prediction += (long long)coefficients[9] * (long long)pDecodedSamples[-10]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; + prediction += coefficients[8] * (drflac_int64)pDecodedSamples[-9]; + prediction += coefficients[9] * (drflac_int64)pDecodedSamples[-10]; } else if (order == 9) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; - prediction += (long long)coefficients[3] * (long long)pDecodedSamples[-4]; - prediction += (long long)coefficients[4] * (long long)pDecodedSamples[-5]; - prediction += (long long)coefficients[5] * (long long)pDecodedSamples[-6]; - prediction += (long long)coefficients[6] * (long long)pDecodedSamples[-7]; - prediction += (long long)coefficients[7] * (long long)pDecodedSamples[-8]; - prediction += (long long)coefficients[8] * (long long)pDecodedSamples[-9]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; + prediction += coefficients[8] * (drflac_int64)pDecodedSamples[-9]; } else if (order == 11) { - prediction = (long long)coefficients[0] * (long long)pDecodedSamples[-1]; - prediction += (long long)coefficients[1] * (long long)pDecodedSamples[-2]; - prediction += (long long)coefficients[2] * (long long)pDecodedSamples[-3]; - prediction += (long long)coefficients[3] * (long long)pDecodedSamples[-4]; - prediction += (long long)coefficients[4] * (long long)pDecodedSamples[-5]; - prediction += (long long)coefficients[5] * (long long)pDecodedSamples[-6]; - prediction += (long long)coefficients[6] * (long long)pDecodedSamples[-7]; - prediction += (long long)coefficients[7] * (long long)pDecodedSamples[-8]; - prediction += (long long)coefficients[8] * (long long)pDecodedSamples[-9]; - prediction += (long long)coefficients[9] * (long long)pDecodedSamples[-10]; - prediction += (long long)coefficients[10] * (long long)pDecodedSamples[-11]; + prediction = coefficients[0] * (drflac_int64)pDecodedSamples[-1]; + prediction += coefficients[1] * (drflac_int64)pDecodedSamples[-2]; + prediction += coefficients[2] * (drflac_int64)pDecodedSamples[-3]; + prediction += coefficients[3] * (drflac_int64)pDecodedSamples[-4]; + prediction += coefficients[4] * (drflac_int64)pDecodedSamples[-5]; + prediction += coefficients[5] * (drflac_int64)pDecodedSamples[-6]; + prediction += coefficients[6] * (drflac_int64)pDecodedSamples[-7]; + prediction += coefficients[7] * (drflac_int64)pDecodedSamples[-8]; + prediction += coefficients[8] * (drflac_int64)pDecodedSamples[-9]; + prediction += coefficients[9] * (drflac_int64)pDecodedSamples[-10]; + prediction += coefficients[10] * (drflac_int64)pDecodedSamples[-11]; } else { + int j; + prediction = 0; - for (int j = 0; j < (int)order; ++j) { - prediction += (long long)coefficients[j] * (long long)pDecodedSamples[-j-1]; + for (j = 0; j < (int)order; ++j) { + prediction += coefficients[j] * (drflac_int64)pDecodedSamples[-j-1]; } } #endif - // Experiment #2. See if we can use a switch and let the compiler optimize it to a single jmp instruction. - // Result: VC++ optimizes this to a single jmp on the 64-bit build, but for some reason the 32-bit version compiles to less efficient - // code. Thus, we use this version on the 64-bit build and the uglier version above for the 32-bit build. If anyone has an idea on how - // I can get VC++ to generate an efficient jump table for the 32-bit build let me know. + /* + VC++ optimizes this to a single jmp instruction, but only the 64-bit build. The 32-bit build generates less efficient code for some + reason. The ugly version above is faster so we'll just switch between the two depending on the target platform. + */ #ifdef DRFLAC_64BIT - long long prediction = 0; - + prediction = 0; switch (order) { - case 32: prediction += (long long)coefficients[31] * (long long)pDecodedSamples[-32]; - case 31: prediction += (long long)coefficients[30] * (long long)pDecodedSamples[-31]; - case 30: prediction += (long long)coefficients[29] * (long long)pDecodedSamples[-30]; - case 29: prediction += (long long)coefficients[28] * (long long)pDecodedSamples[-29]; - case 28: prediction += (long long)coefficients[27] * (long long)pDecodedSamples[-28]; - case 27: prediction += (long long)coefficients[26] * (long long)pDecodedSamples[-27]; - case 26: prediction += (long long)coefficients[25] * (long long)pDecodedSamples[-26]; - case 25: prediction += (long long)coefficients[24] * (long long)pDecodedSamples[-25]; - case 24: prediction += (long long)coefficients[23] * (long long)pDecodedSamples[-24]; - case 23: prediction += (long long)coefficients[22] * (long long)pDecodedSamples[-23]; - case 22: prediction += (long long)coefficients[21] * (long long)pDecodedSamples[-22]; - case 21: prediction += (long long)coefficients[20] * (long long)pDecodedSamples[-21]; - case 20: prediction += (long long)coefficients[19] * (long long)pDecodedSamples[-20]; - case 19: prediction += (long long)coefficients[18] * (long long)pDecodedSamples[-19]; - case 18: prediction += (long long)coefficients[17] * (long long)pDecodedSamples[-18]; - case 17: prediction += (long long)coefficients[16] * (long long)pDecodedSamples[-17]; - case 16: prediction += (long long)coefficients[15] * (long long)pDecodedSamples[-16]; - case 15: prediction += (long long)coefficients[14] * (long long)pDecodedSamples[-15]; - case 14: prediction += (long long)coefficients[13] * (long long)pDecodedSamples[-14]; - case 13: prediction += (long long)coefficients[12] * (long long)pDecodedSamples[-13]; - case 12: prediction += (long long)coefficients[11] * (long long)pDecodedSamples[-12]; - case 11: prediction += (long long)coefficients[10] * (long long)pDecodedSamples[-11]; - case 10: prediction += (long long)coefficients[ 9] * (long long)pDecodedSamples[-10]; - case 9: prediction += (long long)coefficients[ 8] * (long long)pDecodedSamples[- 9]; - case 8: prediction += (long long)coefficients[ 7] * (long long)pDecodedSamples[- 8]; - case 7: prediction += (long long)coefficients[ 6] * (long long)pDecodedSamples[- 7]; - case 6: prediction += (long long)coefficients[ 5] * (long long)pDecodedSamples[- 6]; - case 5: prediction += (long long)coefficients[ 4] * (long long)pDecodedSamples[- 5]; - case 4: prediction += (long long)coefficients[ 3] * (long long)pDecodedSamples[- 4]; - case 3: prediction += (long long)coefficients[ 2] * (long long)pDecodedSamples[- 3]; - case 2: prediction += (long long)coefficients[ 1] * (long long)pDecodedSamples[- 2]; - case 1: prediction += (long long)coefficients[ 0] * (long long)pDecodedSamples[- 1]; + case 32: prediction += coefficients[31] * (drflac_int64)pDecodedSamples[-32]; + case 31: prediction += coefficients[30] * (drflac_int64)pDecodedSamples[-31]; + case 30: prediction += coefficients[29] * (drflac_int64)pDecodedSamples[-30]; + case 29: prediction += coefficients[28] * (drflac_int64)pDecodedSamples[-29]; + case 28: prediction += coefficients[27] * (drflac_int64)pDecodedSamples[-28]; + case 27: prediction += coefficients[26] * (drflac_int64)pDecodedSamples[-27]; + case 26: prediction += coefficients[25] * (drflac_int64)pDecodedSamples[-26]; + case 25: prediction += coefficients[24] * (drflac_int64)pDecodedSamples[-25]; + case 24: prediction += coefficients[23] * (drflac_int64)pDecodedSamples[-24]; + case 23: prediction += coefficients[22] * (drflac_int64)pDecodedSamples[-23]; + case 22: prediction += coefficients[21] * (drflac_int64)pDecodedSamples[-22]; + case 21: prediction += coefficients[20] * (drflac_int64)pDecodedSamples[-21]; + case 20: prediction += coefficients[19] * (drflac_int64)pDecodedSamples[-20]; + case 19: prediction += coefficients[18] * (drflac_int64)pDecodedSamples[-19]; + case 18: prediction += coefficients[17] * (drflac_int64)pDecodedSamples[-18]; + case 17: prediction += coefficients[16] * (drflac_int64)pDecodedSamples[-17]; + case 16: prediction += coefficients[15] * (drflac_int64)pDecodedSamples[-16]; + case 15: prediction += coefficients[14] * (drflac_int64)pDecodedSamples[-15]; + case 14: prediction += coefficients[13] * (drflac_int64)pDecodedSamples[-14]; + case 13: prediction += coefficients[12] * (drflac_int64)pDecodedSamples[-13]; + case 12: prediction += coefficients[11] * (drflac_int64)pDecodedSamples[-12]; + case 11: prediction += coefficients[10] * (drflac_int64)pDecodedSamples[-11]; + case 10: prediction += coefficients[ 9] * (drflac_int64)pDecodedSamples[-10]; + case 9: prediction += coefficients[ 8] * (drflac_int64)pDecodedSamples[- 9]; + case 8: prediction += coefficients[ 7] * (drflac_int64)pDecodedSamples[- 8]; + case 7: prediction += coefficients[ 6] * (drflac_int64)pDecodedSamples[- 7]; + case 6: prediction += coefficients[ 5] * (drflac_int64)pDecodedSamples[- 6]; + case 5: prediction += coefficients[ 4] * (drflac_int64)pDecodedSamples[- 5]; + case 4: prediction += coefficients[ 3] * (drflac_int64)pDecodedSamples[- 4]; + case 3: prediction += coefficients[ 2] * (drflac_int64)pDecodedSamples[- 3]; + case 2: prediction += coefficients[ 1] * (drflac_int64)pDecodedSamples[- 2]; + case 1: prediction += coefficients[ 0] * (drflac_int64)pDecodedSamples[- 1]; } #endif - return (int32_t)(prediction >> shift); + return (drflac_int32)(prediction >> shift); } -// Reads and decodes a string of residual values as Rice codes. The decoder should be sitting on the first bit of the Rice codes. -// -// This is the most frequently called function in the library. It does both the Rice decoding and the prediction in a single loop -// iteration. -static bool drflac__decode_samples_with_residual__rice(drflac* pFlac, unsigned int count, unsigned char riceParam, unsigned int order, int shift, const short* coefficients, int* pSamplesOut) +#if 0 +/* +Reference implementation for reading and decoding samples with residual. This is intentionally left unoptimized for the +sake of readability and should only be used as a reference. +*/ +static drflac_bool32 drflac__decode_samples_with_residual__rice__reference(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) { - assert(pFlac != NULL); - assert(count > 0); - assert(pSamplesOut != NULL); + drflac_uint32 i; - static unsigned int bitOffsetTable[] = { - 0, - 4, - 3, 3, - 2, 2, 2, 2, - 1, 1, 1, 1, 1, 1, 1, 1 - }; + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); - drflac_cache_t riceParamMask = DRFLAC_CACHE_L1_SELECTION_MASK(riceParam); - drflac_cache_t resultHiShift = DRFLAC_CACHE_L1_SIZE_BITS - riceParam; + for (i = 0; i < count; ++i) { + drflac_uint32 zeroCounter = 0; + for (;;) { + drflac_uint8 bit; + if (!drflac__read_uint8(bs, 1, &bit)) { + return DRFLAC_FALSE; + } - for (int i = 0; i < (int)count; ++i) - { - unsigned int zeroCounter = 0; - while (pFlac->cache == 0) { - zeroCounter += (unsigned int)DRFLAC_CACHE_L1_BITS_REMAINING; - if (!drflac__reload_cache(pFlac)) { - return false; + if (bit == 0) { + zeroCounter += 1; + } else { + break; } } - // At this point the cache should not be zero, in which case we know the first set bit should be somewhere in here. There is - // no need for us to perform any cache reloading logic here which should make things much faster. - assert(pFlac->cache != 0); - unsigned int decodedRice; - - unsigned int setBitOffsetPlus1 = bitOffsetTable[DRFLAC_CACHE_L1_SELECT_AND_SHIFT(4)]; - if (setBitOffsetPlus1 > 0) { - decodedRice = (zeroCounter + (setBitOffsetPlus1-1)) << riceParam; + drflac_uint32 decodedRice; + if (riceParam > 0) { + if (!drflac__read_uint32(bs, riceParam, &decodedRice)) { + return DRFLAC_FALSE; + } } else { - if (pFlac->cache == 1) { - setBitOffsetPlus1 = DRFLAC_CACHE_L1_SIZE_BITS; - decodedRice = (zeroCounter + (DRFLAC_CACHE_L1_SIZE_BITS-1)) << riceParam; - } else { - setBitOffsetPlus1 = 5; - for (;;) - { - if ((pFlac->cache & DRFLAC_CACHE_L1_SELECT(setBitOffsetPlus1))) { - decodedRice = (zeroCounter + (setBitOffsetPlus1-1)) << riceParam; - break; - } - - setBitOffsetPlus1 += 1; - } - } + decodedRice = 0; } - - unsigned int bitsLo = 0; - unsigned int riceLength = setBitOffsetPlus1 + riceParam; - if (riceLength < DRFLAC_CACHE_L1_BITS_REMAINING) - { - bitsLo = (unsigned int)((pFlac->cache & (riceParamMask >> setBitOffsetPlus1)) >> (DRFLAC_CACHE_L1_SIZE_BITS - riceLength)); - - pFlac->consumedBits += riceLength; - pFlac->cache <<= riceLength; - } - else - { - pFlac->consumedBits += riceLength; - pFlac->cache <<= setBitOffsetPlus1; - - // It straddles the cached data. It will never cover more than the next chunk. We just read the number in two parts and combine them. - size_t bitCountLo = pFlac->consumedBits - DRFLAC_CACHE_L1_SIZE_BITS; - drflac_cache_t resultHi = pFlac->cache & riceParamMask; // <-- This mask is OK because all bits after the first bits are always zero. - - - if (pFlac->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT) { - pFlac->cache = drflac__be2host__cache_line(pFlac->cacheL2[pFlac->nextL2Line++]); - } else { - // Slow path. We need to fetch more data from the client. - if (!drflac__reload_cache(pFlac)) { - return false; - } - } - - bitsLo = (unsigned int)((resultHi >> resultHiShift) | DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bitCountLo)); - pFlac->consumedBits = bitCountLo; - pFlac->cache <<= bitCountLo; - } - - - decodedRice |= bitsLo; + decodedRice |= (zeroCounter << riceParam); if ((decodedRice & 0x01)) { decodedRice = ~(decodedRice >> 1); } else { - decodedRice = (decodedRice >> 1); + decodedRice = (decodedRice >> 1); } - // In order to properly calculate the prediction when the bits per sample is >16 we need to do it using 64-bit arithmetic. We can assume this - // is probably going to be slower on 32-bit systems so we'll do a more optimized 32-bit version when the bits per sample is low enough. - if (pFlac->currentFrame.bitsPerSample > 16) { - pSamplesOut[i] = ((int)decodedRice + drflac__calculate_prediction(order, shift, coefficients, pSamplesOut + i)); + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + pSamplesOut[i] = decodedRice + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + i); } else { - pSamplesOut[i] = ((int)decodedRice + drflac__calculate_prediction_32(order, shift, coefficients, pSamplesOut + i)); + pSamplesOut[i] = decodedRice + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + i); } } - return true; + return DRFLAC_TRUE; } +#endif - -// Reads and seeks past a string of residual values as Rice codes. The decoder should be sitting on the first bit of the Rice codes. -static bool drflac__read_and_seek_residual__rice(drflac* pFlac, unsigned int count, unsigned char riceParam) +#if 0 +static drflac_bool32 drflac__read_rice_parts__reference(drflac_bs* bs, drflac_uint8 riceParam, drflac_uint32* pZeroCounterOut, drflac_uint32* pRiceParamPartOut) { - assert(pFlac != NULL); - assert(count > 0); + drflac_uint32 zeroCounter = 0; + drflac_uint32 decodedRice; - for (unsigned int i = 0; i < count; ++i) { - if (!drflac__read_and_seek_rice(pFlac, riceParam)) { - return false; + for (;;) { + drflac_uint8 bit; + if (!drflac__read_uint8(bs, 1, &bit)) { + return DRFLAC_FALSE; + } + + if (bit == 0) { + zeroCounter += 1; + } else { + break; } } - return true; + if (riceParam > 0) { + if (!drflac__read_uint32(bs, riceParam, &decodedRice)) { + return DRFLAC_FALSE; + } + } else { + decodedRice = 0; + } + + *pZeroCounterOut = zeroCounter; + *pRiceParamPartOut = decodedRice; + return DRFLAC_TRUE; +} +#endif + +#if 0 +static DRFLAC_INLINE drflac_bool32 drflac__read_rice_parts(drflac_bs* bs, drflac_uint8 riceParam, drflac_uint32* pZeroCounterOut, drflac_uint32* pRiceParamPartOut) +{ + drflac_cache_t riceParamMask; + drflac_uint32 zeroCounter; + drflac_uint32 setBitOffsetPlus1; + drflac_uint32 riceParamPart; + drflac_uint32 riceLength; + + DRFLAC_ASSERT(riceParam > 0); /* <-- riceParam should never be 0. drflac__read_rice_parts__param_equals_zero() should be used instead for this case. */ + + riceParamMask = DRFLAC_CACHE_L1_SELECTION_MASK(riceParam); + + zeroCounter = 0; + while (bs->cache == 0) { + zeroCounter += (drflac_uint32)DRFLAC_CACHE_L1_BITS_REMAINING(bs); + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + } + + setBitOffsetPlus1 = drflac__clz(bs->cache); + zeroCounter += setBitOffsetPlus1; + setBitOffsetPlus1 += 1; + + riceLength = setBitOffsetPlus1 + riceParam; + if (riceLength < DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + riceParamPart = (drflac_uint32)((bs->cache & (riceParamMask >> setBitOffsetPlus1)) >> DRFLAC_CACHE_L1_SELECTION_SHIFT(bs, riceLength)); + + bs->consumedBits += riceLength; + bs->cache <<= riceLength; + } else { + drflac_uint32 bitCountLo; + drflac_cache_t resultHi; + + bs->consumedBits += riceLength; + bs->cache <<= setBitOffsetPlus1 & (DRFLAC_CACHE_L1_SIZE_BITS(bs)-1); /* <-- Equivalent to "if (setBitOffsetPlus1 < DRFLAC_CACHE_L1_SIZE_BITS(bs)) { bs->cache <<= setBitOffsetPlus1; }" */ + + /* It straddles the cached data. It will never cover more than the next chunk. We just read the number in two parts and combine them. */ + bitCountLo = bs->consumedBits - DRFLAC_CACHE_L1_SIZE_BITS(bs); + resultHi = DRFLAC_CACHE_L1_SELECT_AND_SHIFT(bs, riceParam); /* <-- Use DRFLAC_CACHE_L1_SELECT_AND_SHIFT_SAFE() if ever this function allows riceParam=0. */ + + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { +#ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); +#endif + bs->cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs->consumedBits = 0; +#ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs->cache; +#endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + if (bitCountLo > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; + } + } + + riceParamPart = (drflac_uint32)(resultHi | DRFLAC_CACHE_L1_SELECT_AND_SHIFT_SAFE(bs, bitCountLo)); + + bs->consumedBits += bitCountLo; + bs->cache <<= bitCountLo; + } + + pZeroCounterOut[0] = zeroCounter; + pRiceParamPartOut[0] = riceParamPart; + + return DRFLAC_TRUE; +} +#endif + +static DRFLAC_INLINE drflac_bool32 drflac__read_rice_parts_x1(drflac_bs* bs, drflac_uint8 riceParam, drflac_uint32* pZeroCounterOut, drflac_uint32* pRiceParamPartOut) +{ + drflac_uint32 riceParamPlus1 = riceParam + 1; + /*drflac_cache_t riceParamPlus1Mask = DRFLAC_CACHE_L1_SELECTION_MASK(riceParamPlus1);*/ + drflac_uint32 riceParamPlus1Shift = DRFLAC_CACHE_L1_SELECTION_SHIFT(bs, riceParamPlus1); + drflac_uint32 riceParamPlus1MaxConsumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs) - riceParamPlus1; + + /* + The idea here is to use local variables for the cache in an attempt to encourage the compiler to store them in registers. I have + no idea how this will work in practice... + */ + drflac_cache_t bs_cache = bs->cache; + drflac_uint32 bs_consumedBits = bs->consumedBits; + + /* The first thing to do is find the first unset bit. Most likely a bit will be set in the current cache line. */ + drflac_uint32 lzcount = drflac__clz(bs_cache); + if (lzcount < sizeof(bs_cache)*8) { + pZeroCounterOut[0] = lzcount; + + /* + It is most likely that the riceParam part (which comes after the zero counter) is also on this cache line. When extracting + this, we include the set bit from the unary coded part because it simplifies cache management. This bit will be handled + outside of this function at a higher level. + */ + extract_rice_param_part: + bs_cache <<= lzcount; + bs_consumedBits += lzcount; + + if (bs_consumedBits <= riceParamPlus1MaxConsumedBits) { + /* Getting here means the rice parameter part is wholly contained within the current cache line. */ + pRiceParamPartOut[0] = (drflac_uint32)(bs_cache >> riceParamPlus1Shift); + bs_cache <<= riceParamPlus1; + bs_consumedBits += riceParamPlus1; + } else { + drflac_uint32 riceParamPartHi; + drflac_uint32 riceParamPartLo; + drflac_uint32 riceParamPartLoBitCount; + + /* + Getting here means the rice parameter part straddles the cache line. We need to read from the tail of the current cache + line, reload the cache, and then combine it with the head of the next cache line. + */ + + /* Grab the high part of the rice parameter part. */ + riceParamPartHi = (drflac_uint32)(bs_cache >> riceParamPlus1Shift); + + /* Before reloading the cache we need to grab the size in bits of the low part. */ + riceParamPartLoBitCount = bs_consumedBits - riceParamPlus1MaxConsumedBits; + DRFLAC_ASSERT(riceParamPartLoBitCount > 0 && riceParamPartLoBitCount < 32); + + /* Now reload the cache. */ + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + #ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); + #endif + bs_cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs_consumedBits = riceParamPartLoBitCount; + #ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs_cache; + #endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + if (riceParamPartLoBitCount > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; + } + + bs_cache = bs->cache; + bs_consumedBits = bs->consumedBits + riceParamPartLoBitCount; + } + + /* We should now have enough information to construct the rice parameter part. */ + riceParamPartLo = (drflac_uint32)(bs_cache >> (DRFLAC_CACHE_L1_SELECTION_SHIFT(bs, riceParamPartLoBitCount))); + pRiceParamPartOut[0] = riceParamPartHi | riceParamPartLo; + + bs_cache <<= riceParamPartLoBitCount; + } + } else { + /* + Getting here means there are no bits set on the cache line. This is a less optimal case because we just wasted a call + to drflac__clz() and we need to reload the cache. + */ + drflac_uint32 zeroCounter = (drflac_uint32)(DRFLAC_CACHE_L1_SIZE_BITS(bs) - bs_consumedBits); + for (;;) { + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + #ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); + #endif + bs_cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs_consumedBits = 0; + #ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs_cache; + #endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + + bs_cache = bs->cache; + bs_consumedBits = bs->consumedBits; + } + + lzcount = drflac__clz(bs_cache); + zeroCounter += lzcount; + + if (lzcount < sizeof(bs_cache)*8) { + break; + } + } + + pZeroCounterOut[0] = zeroCounter; + goto extract_rice_param_part; + } + + /* Make sure the cache is restored at the end of it all. */ + bs->cache = bs_cache; + bs->consumedBits = bs_consumedBits; + + return DRFLAC_TRUE; } -static bool drflac__decode_samples_with_residual__unencoded(drflac* pFlac, unsigned int count, unsigned char unencodedBitsPerSample, unsigned int order, int shift, const short* coefficients, int* pSamplesOut) +static DRFLAC_INLINE drflac_bool32 drflac__seek_rice_parts(drflac_bs* bs, drflac_uint8 riceParam) { - assert(pFlac != NULL); - assert(count > 0); - assert(unencodedBitsPerSample > 0 && unencodedBitsPerSample <= 32); - assert(pSamplesOut != NULL); + drflac_uint32 riceParamPlus1 = riceParam + 1; + drflac_uint32 riceParamPlus1MaxConsumedBits = DRFLAC_CACHE_L1_SIZE_BITS(bs) - riceParamPlus1; - for (unsigned int i = 0; i < count; ++i) + /* + The idea here is to use local variables for the cache in an attempt to encourage the compiler to store them in registers. I have + no idea how this will work in practice... + */ + drflac_cache_t bs_cache = bs->cache; + drflac_uint32 bs_consumedBits = bs->consumedBits; + + /* The first thing to do is find the first unset bit. Most likely a bit will be set in the current cache line. */ + drflac_uint32 lzcount = drflac__clz(bs_cache); + if (lzcount < sizeof(bs_cache)*8) { + /* + It is most likely that the riceParam part (which comes after the zero counter) is also on this cache line. When extracting + this, we include the set bit from the unary coded part because it simplifies cache management. This bit will be handled + outside of this function at a higher level. + */ + extract_rice_param_part: + bs_cache <<= lzcount; + bs_consumedBits += lzcount; + + if (bs_consumedBits <= riceParamPlus1MaxConsumedBits) { + /* Getting here means the rice parameter part is wholly contained within the current cache line. */ + bs_cache <<= riceParamPlus1; + bs_consumedBits += riceParamPlus1; + } else { + /* + Getting here means the rice parameter part straddles the cache line. We need to read from the tail of the current cache + line, reload the cache, and then combine it with the head of the next cache line. + */ + + /* Before reloading the cache we need to grab the size in bits of the low part. */ + drflac_uint32 riceParamPartLoBitCount = bs_consumedBits - riceParamPlus1MaxConsumedBits; + DRFLAC_ASSERT(riceParamPartLoBitCount > 0 && riceParamPartLoBitCount < 32); + + /* Now reload the cache. */ + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + #ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); + #endif + bs_cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs_consumedBits = riceParamPartLoBitCount; + #ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs_cache; + #endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + + if (riceParamPartLoBitCount > DRFLAC_CACHE_L1_BITS_REMAINING(bs)) { + /* This happens when we get to end of stream */ + return DRFLAC_FALSE; + } + + bs_cache = bs->cache; + bs_consumedBits = bs->consumedBits + riceParamPartLoBitCount; + } + + bs_cache <<= riceParamPartLoBitCount; + } + } else { + /* + Getting here means there are no bits set on the cache line. This is a less optimal case because we just wasted a call + to drflac__clz() and we need to reload the cache. + */ + for (;;) { + if (bs->nextL2Line < DRFLAC_CACHE_L2_LINE_COUNT(bs)) { + #ifndef DR_FLAC_NO_CRC + drflac__update_crc16(bs); + #endif + bs_cache = drflac__be2host__cache_line(bs->cacheL2[bs->nextL2Line++]); + bs_consumedBits = 0; + #ifndef DR_FLAC_NO_CRC + bs->crc16Cache = bs_cache; + #endif + } else { + /* Slow path. We need to fetch more data from the client. */ + if (!drflac__reload_cache(bs)) { + return DRFLAC_FALSE; + } + + bs_cache = bs->cache; + bs_consumedBits = bs->consumedBits; + } + + lzcount = drflac__clz(bs_cache); + if (lzcount < sizeof(bs_cache)*8) { + break; + } + } + + goto extract_rice_param_part; + } + + /* Make sure the cache is restored at the end of it all. */ + bs->cache = bs_cache; + bs->consumedBits = bs_consumedBits; + + return DRFLAC_TRUE; +} + + +static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar_zeroorder(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + drflac_uint32 zeroCountPart0; + drflac_uint32 riceParamPart0; + drflac_uint32 riceParamMask; + drflac_uint32 i; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); + + (void)bitsPerSample; + (void)order; + (void)shift; + (void)coefficients; + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + + i = 0; + while (i < count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart0, &riceParamPart0)) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamPart0 &= riceParamMask; + riceParamPart0 |= (zeroCountPart0 << riceParam); + riceParamPart0 = (riceParamPart0 >> 1) ^ t[riceParamPart0 & 0x01]; + + pSamplesOut[i] = riceParamPart0; + + i += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__scalar(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + drflac_uint32 zeroCountPart0 = 0; + drflac_uint32 zeroCountPart1 = 0; + drflac_uint32 zeroCountPart2 = 0; + drflac_uint32 zeroCountPart3 = 0; + drflac_uint32 riceParamPart0 = 0; + drflac_uint32 riceParamPart1 = 0; + drflac_uint32 riceParamPart2 = 0; + drflac_uint32 riceParamPart3 = 0; + drflac_uint32 riceParamMask; + const drflac_int32* pSamplesOutEnd; + drflac_uint32 i; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); + + if (lpcOrder == 0) { + return drflac__decode_samples_with_residual__rice__scalar_zeroorder(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + pSamplesOutEnd = pSamplesOut + (count & ~3); + + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + while (pSamplesOut < pSamplesOutEnd) { + /* + Rice extraction. It's faster to do this one at a time against local variables than it is to use the x4 version + against an array. Not sure why, but perhaps it's making more efficient use of registers? + */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart0, &riceParamPart0) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart1, &riceParamPart1) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart2, &riceParamPart2) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart3, &riceParamPart3)) { + return DRFLAC_FALSE; + } + + riceParamPart0 &= riceParamMask; + riceParamPart1 &= riceParamMask; + riceParamPart2 &= riceParamMask; + riceParamPart3 &= riceParamMask; + + riceParamPart0 |= (zeroCountPart0 << riceParam); + riceParamPart1 |= (zeroCountPart1 << riceParam); + riceParamPart2 |= (zeroCountPart2 << riceParam); + riceParamPart3 |= (zeroCountPart3 << riceParam); + + riceParamPart0 = (riceParamPart0 >> 1) ^ t[riceParamPart0 & 0x01]; + riceParamPart1 = (riceParamPart1 >> 1) ^ t[riceParamPart1 & 0x01]; + riceParamPart2 = (riceParamPart2 >> 1) ^ t[riceParamPart2 & 0x01]; + riceParamPart3 = (riceParamPart3 >> 1) ^ t[riceParamPart3 & 0x01]; + + pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 0); + pSamplesOut[1] = riceParamPart1 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 1); + pSamplesOut[2] = riceParamPart2 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 2); + pSamplesOut[3] = riceParamPart3 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 3); + + pSamplesOut += 4; + } + } else { + while (pSamplesOut < pSamplesOutEnd) { + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart0, &riceParamPart0) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart1, &riceParamPart1) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart2, &riceParamPart2) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart3, &riceParamPart3)) { + return DRFLAC_FALSE; + } + + riceParamPart0 &= riceParamMask; + riceParamPart1 &= riceParamMask; + riceParamPart2 &= riceParamMask; + riceParamPart3 &= riceParamMask; + + riceParamPart0 |= (zeroCountPart0 << riceParam); + riceParamPart1 |= (zeroCountPart1 << riceParam); + riceParamPart2 |= (zeroCountPart2 << riceParam); + riceParamPart3 |= (zeroCountPart3 << riceParam); + + riceParamPart0 = (riceParamPart0 >> 1) ^ t[riceParamPart0 & 0x01]; + riceParamPart1 = (riceParamPart1 >> 1) ^ t[riceParamPart1 & 0x01]; + riceParamPart2 = (riceParamPart2 >> 1) ^ t[riceParamPart2 & 0x01]; + riceParamPart3 = (riceParamPart3 >> 1) ^ t[riceParamPart3 & 0x01]; + + pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 0); + pSamplesOut[1] = riceParamPart1 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 1); + pSamplesOut[2] = riceParamPart2 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 2); + pSamplesOut[3] = riceParamPart3 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 3); + + pSamplesOut += 4; + } + } + + i = (count & ~3); + while (i < count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountPart0, &riceParamPart0)) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamPart0 &= riceParamMask; + riceParamPart0 |= (zeroCountPart0 << riceParam); + riceParamPart0 = (riceParamPart0 >> 1) ^ t[riceParamPart0 & 0x01]; + /*riceParamPart0 = (riceParamPart0 >> 1) ^ (~(riceParamPart0 & 0x01) + 1);*/ + + /* Sample reconstruction. */ + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + 0); + } else { + pSamplesOut[0] = riceParamPart0 + drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + 0); + } + + i += 1; + pSamplesOut += 1; + } + + return DRFLAC_TRUE; +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE __m128i drflac__mm_packs_interleaved_epi32(__m128i a, __m128i b) +{ + __m128i r; + + /* Pack. */ + r = _mm_packs_epi32(a, b); + + /* a3a2 a1a0 b3b2 b1b0 -> a3a2 b3b2 a1a0 b1b0 */ + r = _mm_shuffle_epi32(r, _MM_SHUFFLE(3, 1, 2, 0)); + + /* a3a2 b3b2 a1a0 b1b0 -> a3b3 a2b2 a1b1 a0b0 */ + r = _mm_shufflehi_epi16(r, _MM_SHUFFLE(3, 1, 2, 0)); + r = _mm_shufflelo_epi16(r, _MM_SHUFFLE(3, 1, 2, 0)); + + return r; +} +#endif + +#if defined(DRFLAC_SUPPORT_SSE41) +static DRFLAC_INLINE __m128i drflac__mm_not_si128(__m128i a) +{ + return _mm_xor_si128(a, _mm_cmpeq_epi32(_mm_setzero_si128(), _mm_setzero_si128())); +} + +static DRFLAC_INLINE __m128i drflac__mm_hadd_epi32(__m128i x) +{ + __m128i x64 = _mm_add_epi32(x, _mm_shuffle_epi32(x, _MM_SHUFFLE(1, 0, 3, 2))); + __m128i x32 = _mm_shufflelo_epi16(x64, _MM_SHUFFLE(1, 0, 3, 2)); + return _mm_add_epi32(x64, x32); +} + +static DRFLAC_INLINE __m128i drflac__mm_hadd_epi64(__m128i x) +{ + return _mm_add_epi64(x, _mm_shuffle_epi32(x, _MM_SHUFFLE(1, 0, 3, 2))); +} + +static DRFLAC_INLINE __m128i drflac__mm_srai_epi64(__m128i x, int count) +{ + /* + To simplify this we are assuming count < 32. This restriction allows us to work on a low side and a high side. The low side + is shifted with zero bits, whereas the right side is shifted with sign bits. + */ + __m128i lo = _mm_srli_epi64(x, count); + __m128i hi = _mm_srai_epi32(x, count); + + hi = _mm_and_si128(hi, _mm_set_epi32(0xFFFFFFFF, 0, 0xFFFFFFFF, 0)); /* The high part needs to have the low part cleared. */ + + return _mm_or_si128(lo, hi); +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__sse41_32(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + int i; + drflac_uint32 riceParamMask; + drflac_int32* pDecodedSamples = pSamplesOut; + drflac_int32* pDecodedSamplesEnd = pSamplesOut + (count & ~3); + drflac_uint32 zeroCountParts0 = 0; + drflac_uint32 zeroCountParts1 = 0; + drflac_uint32 zeroCountParts2 = 0; + drflac_uint32 zeroCountParts3 = 0; + drflac_uint32 riceParamParts0 = 0; + drflac_uint32 riceParamParts1 = 0; + drflac_uint32 riceParamParts2 = 0; + drflac_uint32 riceParamParts3 = 0; + __m128i coefficients128_0; + __m128i coefficients128_4; + __m128i coefficients128_8; + __m128i samples128_0; + __m128i samples128_4; + __m128i samples128_8; + __m128i riceParamMask128; + + const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + riceParamMask128 = _mm_set1_epi32(riceParamMask); + + /* Pre-load. */ + coefficients128_0 = _mm_setzero_si128(); + coefficients128_4 = _mm_setzero_si128(); + coefficients128_8 = _mm_setzero_si128(); + + samples128_0 = _mm_setzero_si128(); + samples128_4 = _mm_setzero_si128(); + samples128_8 = _mm_setzero_si128(); + + /* + Pre-loading the coefficients and prior samples is annoying because we need to ensure we don't try reading more than + what's available in the input buffers. It would be convenient to use a fall-through switch to do this, but this results + in strict aliasing warnings with GCC. To work around this I'm just doing something hacky. This feels a bit convoluted + so I think there's opportunity for this to be simplified. + */ +#if 1 { - if (!drflac__read_int32(pFlac, unencodedBitsPerSample, pSamplesOut + i)) { - return false; + int runningOrder = order; + + /* 0 - 3. */ + if (runningOrder >= 4) { + coefficients128_0 = _mm_loadu_si128((const __m128i*)(coefficients + 0)); + samples128_0 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 4)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_0 = _mm_set_epi32(0, coefficients[2], coefficients[1], coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], pSamplesOut[-2], pSamplesOut[-3], 0); break; + case 2: coefficients128_0 = _mm_set_epi32(0, 0, coefficients[1], coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], pSamplesOut[-2], 0, 0); break; + case 1: coefficients128_0 = _mm_set_epi32(0, 0, 0, coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], 0, 0, 0); break; + } + runningOrder = 0; } - pSamplesOut[i] += drflac__calculate_prediction(order, shift, coefficients, pSamplesOut + i); + /* 4 - 7 */ + if (runningOrder >= 4) { + coefficients128_4 = _mm_loadu_si128((const __m128i*)(coefficients + 4)); + samples128_4 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 8)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_4 = _mm_set_epi32(0, coefficients[6], coefficients[5], coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], pSamplesOut[-6], pSamplesOut[-7], 0); break; + case 2: coefficients128_4 = _mm_set_epi32(0, 0, coefficients[5], coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], pSamplesOut[-6], 0, 0); break; + case 1: coefficients128_4 = _mm_set_epi32(0, 0, 0, coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* 8 - 11 */ + if (runningOrder == 4) { + coefficients128_8 = _mm_loadu_si128((const __m128i*)(coefficients + 8)); + samples128_8 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 12)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_8 = _mm_set_epi32(0, coefficients[10], coefficients[9], coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], pSamplesOut[-10], pSamplesOut[-11], 0); break; + case 2: coefficients128_8 = _mm_set_epi32(0, 0, coefficients[9], coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], pSamplesOut[-10], 0, 0); break; + case 1: coefficients128_8 = _mm_set_epi32(0, 0, 0, coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* Coefficients need to be shuffled for our streaming algorithm below to work. Samples are already in the correct order from the loading routine above. */ + coefficients128_0 = _mm_shuffle_epi32(coefficients128_0, _MM_SHUFFLE(0, 1, 2, 3)); + coefficients128_4 = _mm_shuffle_epi32(coefficients128_4, _MM_SHUFFLE(0, 1, 2, 3)); + coefficients128_8 = _mm_shuffle_epi32(coefficients128_8, _MM_SHUFFLE(0, 1, 2, 3)); + } +#else + /* This causes strict-aliasing warnings with GCC. */ + switch (order) + { + case 12: ((drflac_int32*)&coefficients128_8)[0] = coefficients[11]; ((drflac_int32*)&samples128_8)[0] = pDecodedSamples[-12]; + case 11: ((drflac_int32*)&coefficients128_8)[1] = coefficients[10]; ((drflac_int32*)&samples128_8)[1] = pDecodedSamples[-11]; + case 10: ((drflac_int32*)&coefficients128_8)[2] = coefficients[ 9]; ((drflac_int32*)&samples128_8)[2] = pDecodedSamples[-10]; + case 9: ((drflac_int32*)&coefficients128_8)[3] = coefficients[ 8]; ((drflac_int32*)&samples128_8)[3] = pDecodedSamples[- 9]; + case 8: ((drflac_int32*)&coefficients128_4)[0] = coefficients[ 7]; ((drflac_int32*)&samples128_4)[0] = pDecodedSamples[- 8]; + case 7: ((drflac_int32*)&coefficients128_4)[1] = coefficients[ 6]; ((drflac_int32*)&samples128_4)[1] = pDecodedSamples[- 7]; + case 6: ((drflac_int32*)&coefficients128_4)[2] = coefficients[ 5]; ((drflac_int32*)&samples128_4)[2] = pDecodedSamples[- 6]; + case 5: ((drflac_int32*)&coefficients128_4)[3] = coefficients[ 4]; ((drflac_int32*)&samples128_4)[3] = pDecodedSamples[- 5]; + case 4: ((drflac_int32*)&coefficients128_0)[0] = coefficients[ 3]; ((drflac_int32*)&samples128_0)[0] = pDecodedSamples[- 4]; + case 3: ((drflac_int32*)&coefficients128_0)[1] = coefficients[ 2]; ((drflac_int32*)&samples128_0)[1] = pDecodedSamples[- 3]; + case 2: ((drflac_int32*)&coefficients128_0)[2] = coefficients[ 1]; ((drflac_int32*)&samples128_0)[2] = pDecodedSamples[- 2]; + case 1: ((drflac_int32*)&coefficients128_0)[3] = coefficients[ 0]; ((drflac_int32*)&samples128_0)[3] = pDecodedSamples[- 1]; + } +#endif + + /* For this version we are doing one sample at a time. */ + while (pDecodedSamples < pDecodedSamplesEnd) { + __m128i prediction128; + __m128i zeroCountPart128; + __m128i riceParamPart128; + + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts0, &riceParamParts0) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts1, &riceParamParts1) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts2, &riceParamParts2) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts3, &riceParamParts3)) { + return DRFLAC_FALSE; + } + + zeroCountPart128 = _mm_set_epi32(zeroCountParts3, zeroCountParts2, zeroCountParts1, zeroCountParts0); + riceParamPart128 = _mm_set_epi32(riceParamParts3, riceParamParts2, riceParamParts1, riceParamParts0); + + riceParamPart128 = _mm_and_si128(riceParamPart128, riceParamMask128); + riceParamPart128 = _mm_or_si128(riceParamPart128, _mm_slli_epi32(zeroCountPart128, riceParam)); + riceParamPart128 = _mm_xor_si128(_mm_srli_epi32(riceParamPart128, 1), _mm_add_epi32(drflac__mm_not_si128(_mm_and_si128(riceParamPart128, _mm_set1_epi32(0x01))), _mm_set1_epi32(0x01))); /* <-- SSE2 compatible */ + /*riceParamPart128 = _mm_xor_si128(_mm_srli_epi32(riceParamPart128, 1), _mm_mullo_epi32(_mm_and_si128(riceParamPart128, _mm_set1_epi32(0x01)), _mm_set1_epi32(0xFFFFFFFF)));*/ /* <-- Only supported from SSE4.1 and is slower in my testing... */ + + if (order <= 4) { + for (i = 0; i < 4; i += 1) { + prediction128 = _mm_mullo_epi32(coefficients128_0, samples128_0); + + /* Horizontal add and shift. */ + prediction128 = drflac__mm_hadd_epi32(prediction128); + prediction128 = _mm_srai_epi32(prediction128, shift); + prediction128 = _mm_add_epi32(riceParamPart128, prediction128); + + samples128_0 = _mm_alignr_epi8(prediction128, samples128_0, 4); + riceParamPart128 = _mm_alignr_epi8(_mm_setzero_si128(), riceParamPart128, 4); + } + } else if (order <= 8) { + for (i = 0; i < 4; i += 1) { + prediction128 = _mm_mullo_epi32(coefficients128_4, samples128_4); + prediction128 = _mm_add_epi32(prediction128, _mm_mullo_epi32(coefficients128_0, samples128_0)); + + /* Horizontal add and shift. */ + prediction128 = drflac__mm_hadd_epi32(prediction128); + prediction128 = _mm_srai_epi32(prediction128, shift); + prediction128 = _mm_add_epi32(riceParamPart128, prediction128); + + samples128_4 = _mm_alignr_epi8(samples128_0, samples128_4, 4); + samples128_0 = _mm_alignr_epi8(prediction128, samples128_0, 4); + riceParamPart128 = _mm_alignr_epi8(_mm_setzero_si128(), riceParamPart128, 4); + } + } else { + for (i = 0; i < 4; i += 1) { + prediction128 = _mm_mullo_epi32(coefficients128_8, samples128_8); + prediction128 = _mm_add_epi32(prediction128, _mm_mullo_epi32(coefficients128_4, samples128_4)); + prediction128 = _mm_add_epi32(prediction128, _mm_mullo_epi32(coefficients128_0, samples128_0)); + + /* Horizontal add and shift. */ + prediction128 = drflac__mm_hadd_epi32(prediction128); + prediction128 = _mm_srai_epi32(prediction128, shift); + prediction128 = _mm_add_epi32(riceParamPart128, prediction128); + + samples128_8 = _mm_alignr_epi8(samples128_4, samples128_8, 4); + samples128_4 = _mm_alignr_epi8(samples128_0, samples128_4, 4); + samples128_0 = _mm_alignr_epi8(prediction128, samples128_0, 4); + riceParamPart128 = _mm_alignr_epi8(_mm_setzero_si128(), riceParamPart128, 4); + } + } + + /* We store samples in groups of 4. */ + _mm_storeu_si128((__m128i*)pDecodedSamples, samples128_0); + pDecodedSamples += 4; } - return true; + /* Make sure we process the last few samples. */ + i = (count & ~3); + while (i < (int)count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts0, &riceParamParts0)) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamParts0 &= riceParamMask; + riceParamParts0 |= (zeroCountParts0 << riceParam); + riceParamParts0 = (riceParamParts0 >> 1) ^ t[riceParamParts0 & 0x01]; + + /* Sample reconstruction. */ + pDecodedSamples[0] = riceParamParts0 + drflac__calculate_prediction_32(order, shift, coefficients, pDecodedSamples); + + i += 1; + pDecodedSamples += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__sse41_64(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + int i; + drflac_uint32 riceParamMask; + drflac_int32* pDecodedSamples = pSamplesOut; + drflac_int32* pDecodedSamplesEnd = pSamplesOut + (count & ~3); + drflac_uint32 zeroCountParts0 = 0; + drflac_uint32 zeroCountParts1 = 0; + drflac_uint32 zeroCountParts2 = 0; + drflac_uint32 zeroCountParts3 = 0; + drflac_uint32 riceParamParts0 = 0; + drflac_uint32 riceParamParts1 = 0; + drflac_uint32 riceParamParts2 = 0; + drflac_uint32 riceParamParts3 = 0; + __m128i coefficients128_0; + __m128i coefficients128_4; + __m128i coefficients128_8; + __m128i samples128_0; + __m128i samples128_4; + __m128i samples128_8; + __m128i prediction128; + __m128i riceParamMask128; + + const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + + DRFLAC_ASSERT(order <= 12); + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + riceParamMask128 = _mm_set1_epi32(riceParamMask); + + prediction128 = _mm_setzero_si128(); + + /* Pre-load. */ + coefficients128_0 = _mm_setzero_si128(); + coefficients128_4 = _mm_setzero_si128(); + coefficients128_8 = _mm_setzero_si128(); + + samples128_0 = _mm_setzero_si128(); + samples128_4 = _mm_setzero_si128(); + samples128_8 = _mm_setzero_si128(); + +#if 1 + { + int runningOrder = order; + + /* 0 - 3. */ + if (runningOrder >= 4) { + coefficients128_0 = _mm_loadu_si128((const __m128i*)(coefficients + 0)); + samples128_0 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 4)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_0 = _mm_set_epi32(0, coefficients[2], coefficients[1], coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], pSamplesOut[-2], pSamplesOut[-3], 0); break; + case 2: coefficients128_0 = _mm_set_epi32(0, 0, coefficients[1], coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], pSamplesOut[-2], 0, 0); break; + case 1: coefficients128_0 = _mm_set_epi32(0, 0, 0, coefficients[0]); samples128_0 = _mm_set_epi32(pSamplesOut[-1], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* 4 - 7 */ + if (runningOrder >= 4) { + coefficients128_4 = _mm_loadu_si128((const __m128i*)(coefficients + 4)); + samples128_4 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 8)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_4 = _mm_set_epi32(0, coefficients[6], coefficients[5], coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], pSamplesOut[-6], pSamplesOut[-7], 0); break; + case 2: coefficients128_4 = _mm_set_epi32(0, 0, coefficients[5], coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], pSamplesOut[-6], 0, 0); break; + case 1: coefficients128_4 = _mm_set_epi32(0, 0, 0, coefficients[4]); samples128_4 = _mm_set_epi32(pSamplesOut[-5], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* 8 - 11 */ + if (runningOrder == 4) { + coefficients128_8 = _mm_loadu_si128((const __m128i*)(coefficients + 8)); + samples128_8 = _mm_loadu_si128((const __m128i*)(pSamplesOut - 12)); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: coefficients128_8 = _mm_set_epi32(0, coefficients[10], coefficients[9], coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], pSamplesOut[-10], pSamplesOut[-11], 0); break; + case 2: coefficients128_8 = _mm_set_epi32(0, 0, coefficients[9], coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], pSamplesOut[-10], 0, 0); break; + case 1: coefficients128_8 = _mm_set_epi32(0, 0, 0, coefficients[8]); samples128_8 = _mm_set_epi32(pSamplesOut[-9], 0, 0, 0); break; + } + runningOrder = 0; + } + + /* Coefficients need to be shuffled for our streaming algorithm below to work. Samples are already in the correct order from the loading routine above. */ + coefficients128_0 = _mm_shuffle_epi32(coefficients128_0, _MM_SHUFFLE(0, 1, 2, 3)); + coefficients128_4 = _mm_shuffle_epi32(coefficients128_4, _MM_SHUFFLE(0, 1, 2, 3)); + coefficients128_8 = _mm_shuffle_epi32(coefficients128_8, _MM_SHUFFLE(0, 1, 2, 3)); + } +#else + switch (order) + { + case 12: ((drflac_int32*)&coefficients128_8)[0] = coefficients[11]; ((drflac_int32*)&samples128_8)[0] = pDecodedSamples[-12]; + case 11: ((drflac_int32*)&coefficients128_8)[1] = coefficients[10]; ((drflac_int32*)&samples128_8)[1] = pDecodedSamples[-11]; + case 10: ((drflac_int32*)&coefficients128_8)[2] = coefficients[ 9]; ((drflac_int32*)&samples128_8)[2] = pDecodedSamples[-10]; + case 9: ((drflac_int32*)&coefficients128_8)[3] = coefficients[ 8]; ((drflac_int32*)&samples128_8)[3] = pDecodedSamples[- 9]; + case 8: ((drflac_int32*)&coefficients128_4)[0] = coefficients[ 7]; ((drflac_int32*)&samples128_4)[0] = pDecodedSamples[- 8]; + case 7: ((drflac_int32*)&coefficients128_4)[1] = coefficients[ 6]; ((drflac_int32*)&samples128_4)[1] = pDecodedSamples[- 7]; + case 6: ((drflac_int32*)&coefficients128_4)[2] = coefficients[ 5]; ((drflac_int32*)&samples128_4)[2] = pDecodedSamples[- 6]; + case 5: ((drflac_int32*)&coefficients128_4)[3] = coefficients[ 4]; ((drflac_int32*)&samples128_4)[3] = pDecodedSamples[- 5]; + case 4: ((drflac_int32*)&coefficients128_0)[0] = coefficients[ 3]; ((drflac_int32*)&samples128_0)[0] = pDecodedSamples[- 4]; + case 3: ((drflac_int32*)&coefficients128_0)[1] = coefficients[ 2]; ((drflac_int32*)&samples128_0)[1] = pDecodedSamples[- 3]; + case 2: ((drflac_int32*)&coefficients128_0)[2] = coefficients[ 1]; ((drflac_int32*)&samples128_0)[2] = pDecodedSamples[- 2]; + case 1: ((drflac_int32*)&coefficients128_0)[3] = coefficients[ 0]; ((drflac_int32*)&samples128_0)[3] = pDecodedSamples[- 1]; + } +#endif + + /* For this version we are doing one sample at a time. */ + while (pDecodedSamples < pDecodedSamplesEnd) { + __m128i zeroCountPart128; + __m128i riceParamPart128; + + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts0, &riceParamParts0) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts1, &riceParamParts1) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts2, &riceParamParts2) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts3, &riceParamParts3)) { + return DRFLAC_FALSE; + } + + zeroCountPart128 = _mm_set_epi32(zeroCountParts3, zeroCountParts2, zeroCountParts1, zeroCountParts0); + riceParamPart128 = _mm_set_epi32(riceParamParts3, riceParamParts2, riceParamParts1, riceParamParts0); + + riceParamPart128 = _mm_and_si128(riceParamPart128, riceParamMask128); + riceParamPart128 = _mm_or_si128(riceParamPart128, _mm_slli_epi32(zeroCountPart128, riceParam)); + riceParamPart128 = _mm_xor_si128(_mm_srli_epi32(riceParamPart128, 1), _mm_add_epi32(drflac__mm_not_si128(_mm_and_si128(riceParamPart128, _mm_set1_epi32(1))), _mm_set1_epi32(1))); + + for (i = 0; i < 4; i += 1) { + prediction128 = _mm_xor_si128(prediction128, prediction128); /* Reset to 0. */ + + switch (order) + { + case 12: + case 11: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_8, _MM_SHUFFLE(1, 1, 0, 0)), _mm_shuffle_epi32(samples128_8, _MM_SHUFFLE(1, 1, 0, 0)))); + case 10: + case 9: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_8, _MM_SHUFFLE(3, 3, 2, 2)), _mm_shuffle_epi32(samples128_8, _MM_SHUFFLE(3, 3, 2, 2)))); + case 8: + case 7: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_4, _MM_SHUFFLE(1, 1, 0, 0)), _mm_shuffle_epi32(samples128_4, _MM_SHUFFLE(1, 1, 0, 0)))); + case 6: + case 5: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_4, _MM_SHUFFLE(3, 3, 2, 2)), _mm_shuffle_epi32(samples128_4, _MM_SHUFFLE(3, 3, 2, 2)))); + case 4: + case 3: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_0, _MM_SHUFFLE(1, 1, 0, 0)), _mm_shuffle_epi32(samples128_0, _MM_SHUFFLE(1, 1, 0, 0)))); + case 2: + case 1: prediction128 = _mm_add_epi64(prediction128, _mm_mul_epi32(_mm_shuffle_epi32(coefficients128_0, _MM_SHUFFLE(3, 3, 2, 2)), _mm_shuffle_epi32(samples128_0, _MM_SHUFFLE(3, 3, 2, 2)))); + } + + /* Horizontal add and shift. */ + prediction128 = drflac__mm_hadd_epi64(prediction128); + prediction128 = drflac__mm_srai_epi64(prediction128, shift); + prediction128 = _mm_add_epi32(riceParamPart128, prediction128); + + /* Our value should be sitting in prediction128[0]. We need to combine this with our SSE samples. */ + samples128_8 = _mm_alignr_epi8(samples128_4, samples128_8, 4); + samples128_4 = _mm_alignr_epi8(samples128_0, samples128_4, 4); + samples128_0 = _mm_alignr_epi8(prediction128, samples128_0, 4); + + /* Slide our rice parameter down so that the value in position 0 contains the next one to process. */ + riceParamPart128 = _mm_alignr_epi8(_mm_setzero_si128(), riceParamPart128, 4); + } + + /* We store samples in groups of 4. */ + _mm_storeu_si128((__m128i*)pDecodedSamples, samples128_0); + pDecodedSamples += 4; + } + + /* Make sure we process the last few samples. */ + i = (count & ~3); + while (i < (int)count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts0, &riceParamParts0)) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamParts0 &= riceParamMask; + riceParamParts0 |= (zeroCountParts0 << riceParam); + riceParamParts0 = (riceParamParts0 >> 1) ^ t[riceParamParts0 & 0x01]; + + /* Sample reconstruction. */ + pDecodedSamples[0] = riceParamParts0 + drflac__calculate_prediction_64(order, shift, coefficients, pDecodedSamples); + + i += 1; + pDecodedSamples += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__sse41(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); + + /* In my testing the order is rarely > 12, so in this case I'm going to simplify the SSE implementation by only handling order <= 12. */ + if (lpcOrder > 0 && lpcOrder <= 12) { + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + return drflac__decode_samples_with_residual__rice__sse41_64(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } else { + return drflac__decode_samples_with_residual__rice__sse41_32(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } + } else { + return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac__vst2q_s32(drflac_int32* p, int32x4x2_t x) +{ + vst1q_s32(p+0, x.val[0]); + vst1q_s32(p+4, x.val[1]); +} + +static DRFLAC_INLINE void drflac__vst2q_u32(drflac_uint32* p, uint32x4x2_t x) +{ + vst1q_u32(p+0, x.val[0]); + vst1q_u32(p+4, x.val[1]); +} + +static DRFLAC_INLINE void drflac__vst2q_f32(float* p, float32x4x2_t x) +{ + vst1q_f32(p+0, x.val[0]); + vst1q_f32(p+4, x.val[1]); +} + +static DRFLAC_INLINE void drflac__vst2q_s16(drflac_int16* p, int16x4x2_t x) +{ + vst1q_s16(p, vcombine_s16(x.val[0], x.val[1])); +} + +static DRFLAC_INLINE void drflac__vst2q_u16(drflac_uint16* p, uint16x4x2_t x) +{ + vst1q_u16(p, vcombine_u16(x.val[0], x.val[1])); +} + +static DRFLAC_INLINE int32x4_t drflac__vdupq_n_s32x4(drflac_int32 x3, drflac_int32 x2, drflac_int32 x1, drflac_int32 x0) +{ + drflac_int32 x[4]; + x[3] = x3; + x[2] = x2; + x[1] = x1; + x[0] = x0; + return vld1q_s32(x); +} + +static DRFLAC_INLINE int32x4_t drflac__valignrq_s32_1(int32x4_t a, int32x4_t b) +{ + /* Equivalent to SSE's _mm_alignr_epi8(a, b, 4) */ + + /* Reference */ + /*return drflac__vdupq_n_s32x4( + vgetq_lane_s32(a, 0), + vgetq_lane_s32(b, 3), + vgetq_lane_s32(b, 2), + vgetq_lane_s32(b, 1) + );*/ + + return vextq_s32(b, a, 1); +} + +static DRFLAC_INLINE uint32x4_t drflac__valignrq_u32_1(uint32x4_t a, uint32x4_t b) +{ + /* Equivalent to SSE's _mm_alignr_epi8(a, b, 4) */ + + /* Reference */ + /*return drflac__vdupq_n_s32x4( + vgetq_lane_s32(a, 0), + vgetq_lane_s32(b, 3), + vgetq_lane_s32(b, 2), + vgetq_lane_s32(b, 1) + );*/ + + return vextq_u32(b, a, 1); +} + +static DRFLAC_INLINE int32x2_t drflac__vhaddq_s32(int32x4_t x) +{ + /* The sum must end up in position 0. */ + + /* Reference */ + /*return vdupq_n_s32( + vgetq_lane_s32(x, 3) + + vgetq_lane_s32(x, 2) + + vgetq_lane_s32(x, 1) + + vgetq_lane_s32(x, 0) + );*/ + + int32x2_t r = vadd_s32(vget_high_s32(x), vget_low_s32(x)); + return vpadd_s32(r, r); +} + +static DRFLAC_INLINE int64x1_t drflac__vhaddq_s64(int64x2_t x) +{ + return vadd_s64(vget_high_s64(x), vget_low_s64(x)); +} + +static DRFLAC_INLINE int32x4_t drflac__vrevq_s32(int32x4_t x) +{ + /* Reference */ + /*return drflac__vdupq_n_s32x4( + vgetq_lane_s32(x, 0), + vgetq_lane_s32(x, 1), + vgetq_lane_s32(x, 2), + vgetq_lane_s32(x, 3) + );*/ + + return vrev64q_s32(vcombine_s32(vget_high_s32(x), vget_low_s32(x))); +} + +static DRFLAC_INLINE int32x4_t drflac__vnotq_s32(int32x4_t x) +{ + return veorq_s32(x, vdupq_n_s32(0xFFFFFFFF)); +} + +static DRFLAC_INLINE uint32x4_t drflac__vnotq_u32(uint32x4_t x) +{ + return veorq_u32(x, vdupq_n_u32(0xFFFFFFFF)); +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__neon_32(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + int i; + drflac_uint32 riceParamMask; + drflac_int32* pDecodedSamples = pSamplesOut; + drflac_int32* pDecodedSamplesEnd = pSamplesOut + (count & ~3); + drflac_uint32 zeroCountParts[4]; + drflac_uint32 riceParamParts[4]; + int32x4_t coefficients128_0; + int32x4_t coefficients128_4; + int32x4_t coefficients128_8; + int32x4_t samples128_0; + int32x4_t samples128_4; + int32x4_t samples128_8; + uint32x4_t riceParamMask128; + int32x4_t riceParam128; + int32x2_t shift64; + uint32x4_t one128; + + const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + riceParamMask128 = vdupq_n_u32(riceParamMask); + + riceParam128 = vdupq_n_s32(riceParam); + shift64 = vdup_n_s32(-shift); /* Negate the shift because we'll be doing a variable shift using vshlq_s32(). */ + one128 = vdupq_n_u32(1); + + /* + Pre-loading the coefficients and prior samples is annoying because we need to ensure we don't try reading more than + what's available in the input buffers. It would be conenient to use a fall-through switch to do this, but this results + in strict aliasing warnings with GCC. To work around this I'm just doing something hacky. This feels a bit convoluted + so I think there's opportunity for this to be simplified. + */ + { + int runningOrder = order; + drflac_int32 tempC[4] = {0, 0, 0, 0}; + drflac_int32 tempS[4] = {0, 0, 0, 0}; + + /* 0 - 3. */ + if (runningOrder >= 4) { + coefficients128_0 = vld1q_s32(coefficients + 0); + samples128_0 = vld1q_s32(pSamplesOut - 4); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[2]; tempS[1] = pSamplesOut[-3]; /* fallthrough */ + case 2: tempC[1] = coefficients[1]; tempS[2] = pSamplesOut[-2]; /* fallthrough */ + case 1: tempC[0] = coefficients[0]; tempS[3] = pSamplesOut[-1]; /* fallthrough */ + } + + coefficients128_0 = vld1q_s32(tempC); + samples128_0 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* 4 - 7 */ + if (runningOrder >= 4) { + coefficients128_4 = vld1q_s32(coefficients + 4); + samples128_4 = vld1q_s32(pSamplesOut - 8); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[6]; tempS[1] = pSamplesOut[-7]; /* fallthrough */ + case 2: tempC[1] = coefficients[5]; tempS[2] = pSamplesOut[-6]; /* fallthrough */ + case 1: tempC[0] = coefficients[4]; tempS[3] = pSamplesOut[-5]; /* fallthrough */ + } + + coefficients128_4 = vld1q_s32(tempC); + samples128_4 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* 8 - 11 */ + if (runningOrder == 4) { + coefficients128_8 = vld1q_s32(coefficients + 8); + samples128_8 = vld1q_s32(pSamplesOut - 12); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[10]; tempS[1] = pSamplesOut[-11]; /* fallthrough */ + case 2: tempC[1] = coefficients[ 9]; tempS[2] = pSamplesOut[-10]; /* fallthrough */ + case 1: tempC[0] = coefficients[ 8]; tempS[3] = pSamplesOut[- 9]; /* fallthrough */ + } + + coefficients128_8 = vld1q_s32(tempC); + samples128_8 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* Coefficients need to be shuffled for our streaming algorithm below to work. Samples are already in the correct order from the loading routine above. */ + coefficients128_0 = drflac__vrevq_s32(coefficients128_0); + coefficients128_4 = drflac__vrevq_s32(coefficients128_4); + coefficients128_8 = drflac__vrevq_s32(coefficients128_8); + } + + /* For this version we are doing one sample at a time. */ + while (pDecodedSamples < pDecodedSamplesEnd) { + int32x4_t prediction128; + int32x2_t prediction64; + uint32x4_t zeroCountPart128; + uint32x4_t riceParamPart128; + + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[0], &riceParamParts[0]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[1], &riceParamParts[1]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[2], &riceParamParts[2]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[3], &riceParamParts[3])) { + return DRFLAC_FALSE; + } + + zeroCountPart128 = vld1q_u32(zeroCountParts); + riceParamPart128 = vld1q_u32(riceParamParts); + + riceParamPart128 = vandq_u32(riceParamPart128, riceParamMask128); + riceParamPart128 = vorrq_u32(riceParamPart128, vshlq_u32(zeroCountPart128, riceParam128)); + riceParamPart128 = veorq_u32(vshrq_n_u32(riceParamPart128, 1), vaddq_u32(drflac__vnotq_u32(vandq_u32(riceParamPart128, one128)), one128)); + + if (order <= 4) { + for (i = 0; i < 4; i += 1) { + prediction128 = vmulq_s32(coefficients128_0, samples128_0); + + /* Horizontal add and shift. */ + prediction64 = drflac__vhaddq_s32(prediction128); + prediction64 = vshl_s32(prediction64, shift64); + prediction64 = vadd_s32(prediction64, vget_low_s32(vreinterpretq_s32_u32(riceParamPart128))); + + samples128_0 = drflac__valignrq_s32_1(vcombine_s32(prediction64, vdup_n_s32(0)), samples128_0); + riceParamPart128 = drflac__valignrq_u32_1(vdupq_n_u32(0), riceParamPart128); + } + } else if (order <= 8) { + for (i = 0; i < 4; i += 1) { + prediction128 = vmulq_s32(coefficients128_4, samples128_4); + prediction128 = vmlaq_s32(prediction128, coefficients128_0, samples128_0); + + /* Horizontal add and shift. */ + prediction64 = drflac__vhaddq_s32(prediction128); + prediction64 = vshl_s32(prediction64, shift64); + prediction64 = vadd_s32(prediction64, vget_low_s32(vreinterpretq_s32_u32(riceParamPart128))); + + samples128_4 = drflac__valignrq_s32_1(samples128_0, samples128_4); + samples128_0 = drflac__valignrq_s32_1(vcombine_s32(prediction64, vdup_n_s32(0)), samples128_0); + riceParamPart128 = drflac__valignrq_u32_1(vdupq_n_u32(0), riceParamPart128); + } + } else { + for (i = 0; i < 4; i += 1) { + prediction128 = vmulq_s32(coefficients128_8, samples128_8); + prediction128 = vmlaq_s32(prediction128, coefficients128_4, samples128_4); + prediction128 = vmlaq_s32(prediction128, coefficients128_0, samples128_0); + + /* Horizontal add and shift. */ + prediction64 = drflac__vhaddq_s32(prediction128); + prediction64 = vshl_s32(prediction64, shift64); + prediction64 = vadd_s32(prediction64, vget_low_s32(vreinterpretq_s32_u32(riceParamPart128))); + + samples128_8 = drflac__valignrq_s32_1(samples128_4, samples128_8); + samples128_4 = drflac__valignrq_s32_1(samples128_0, samples128_4); + samples128_0 = drflac__valignrq_s32_1(vcombine_s32(prediction64, vdup_n_s32(0)), samples128_0); + riceParamPart128 = drflac__valignrq_u32_1(vdupq_n_u32(0), riceParamPart128); + } + } + + /* We store samples in groups of 4. */ + vst1q_s32(pDecodedSamples, samples128_0); + pDecodedSamples += 4; + } + + /* Make sure we process the last few samples. */ + i = (count & ~3); + while (i < (int)count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[0], &riceParamParts[0])) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamParts[0] &= riceParamMask; + riceParamParts[0] |= (zeroCountParts[0] << riceParam); + riceParamParts[0] = (riceParamParts[0] >> 1) ^ t[riceParamParts[0] & 0x01]; + + /* Sample reconstruction. */ + pDecodedSamples[0] = riceParamParts[0] + drflac__calculate_prediction_32(order, shift, coefficients, pDecodedSamples); + + i += 1; + pDecodedSamples += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__neon_64(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 order, drflac_int32 shift, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + int i; + drflac_uint32 riceParamMask; + drflac_int32* pDecodedSamples = pSamplesOut; + drflac_int32* pDecodedSamplesEnd = pSamplesOut + (count & ~3); + drflac_uint32 zeroCountParts[4]; + drflac_uint32 riceParamParts[4]; + int32x4_t coefficients128_0; + int32x4_t coefficients128_4; + int32x4_t coefficients128_8; + int32x4_t samples128_0; + int32x4_t samples128_4; + int32x4_t samples128_8; + uint32x4_t riceParamMask128; + int32x4_t riceParam128; + int64x1_t shift64; + uint32x4_t one128; + int64x2_t prediction128 = { 0 }; + uint32x4_t zeroCountPart128; + uint32x4_t riceParamPart128; + + const drflac_uint32 t[2] = {0x00000000, 0xFFFFFFFF}; + + riceParamMask = (drflac_uint32)~((~0UL) << riceParam); + riceParamMask128 = vdupq_n_u32(riceParamMask); + + riceParam128 = vdupq_n_s32(riceParam); + shift64 = vdup_n_s64(-shift); /* Negate the shift because we'll be doing a variable shift using vshlq_s32(). */ + one128 = vdupq_n_u32(1); + + /* + Pre-loading the coefficients and prior samples is annoying because we need to ensure we don't try reading more than + what's available in the input buffers. It would be convenient to use a fall-through switch to do this, but this results + in strict aliasing warnings with GCC. To work around this I'm just doing something hacky. This feels a bit convoluted + so I think there's opportunity for this to be simplified. + */ + { + int runningOrder = order; + drflac_int32 tempC[4] = {0, 0, 0, 0}; + drflac_int32 tempS[4] = {0, 0, 0, 0}; + + /* 0 - 3. */ + if (runningOrder >= 4) { + coefficients128_0 = vld1q_s32(coefficients + 0); + samples128_0 = vld1q_s32(pSamplesOut - 4); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[2]; tempS[1] = pSamplesOut[-3]; /* fallthrough */ + case 2: tempC[1] = coefficients[1]; tempS[2] = pSamplesOut[-2]; /* fallthrough */ + case 1: tempC[0] = coefficients[0]; tempS[3] = pSamplesOut[-1]; /* fallthrough */ + } + + coefficients128_0 = vld1q_s32(tempC); + samples128_0 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* 4 - 7 */ + if (runningOrder >= 4) { + coefficients128_4 = vld1q_s32(coefficients + 4); + samples128_4 = vld1q_s32(pSamplesOut - 8); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[6]; tempS[1] = pSamplesOut[-7]; /* fallthrough */ + case 2: tempC[1] = coefficients[5]; tempS[2] = pSamplesOut[-6]; /* fallthrough */ + case 1: tempC[0] = coefficients[4]; tempS[3] = pSamplesOut[-5]; /* fallthrough */ + } + + coefficients128_4 = vld1q_s32(tempC); + samples128_4 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* 8 - 11 */ + if (runningOrder == 4) { + coefficients128_8 = vld1q_s32(coefficients + 8); + samples128_8 = vld1q_s32(pSamplesOut - 12); + runningOrder -= 4; + } else { + switch (runningOrder) { + case 3: tempC[2] = coefficients[10]; tempS[1] = pSamplesOut[-11]; /* fallthrough */ + case 2: tempC[1] = coefficients[ 9]; tempS[2] = pSamplesOut[-10]; /* fallthrough */ + case 1: tempC[0] = coefficients[ 8]; tempS[3] = pSamplesOut[- 9]; /* fallthrough */ + } + + coefficients128_8 = vld1q_s32(tempC); + samples128_8 = vld1q_s32(tempS); + runningOrder = 0; + } + + /* Coefficients need to be shuffled for our streaming algorithm below to work. Samples are already in the correct order from the loading routine above. */ + coefficients128_0 = drflac__vrevq_s32(coefficients128_0); + coefficients128_4 = drflac__vrevq_s32(coefficients128_4); + coefficients128_8 = drflac__vrevq_s32(coefficients128_8); + } + + /* For this version we are doing one sample at a time. */ + while (pDecodedSamples < pDecodedSamplesEnd) { + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[0], &riceParamParts[0]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[1], &riceParamParts[1]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[2], &riceParamParts[2]) || + !drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[3], &riceParamParts[3])) { + return DRFLAC_FALSE; + } + + zeroCountPart128 = vld1q_u32(zeroCountParts); + riceParamPart128 = vld1q_u32(riceParamParts); + + riceParamPart128 = vandq_u32(riceParamPart128, riceParamMask128); + riceParamPart128 = vorrq_u32(riceParamPart128, vshlq_u32(zeroCountPart128, riceParam128)); + riceParamPart128 = veorq_u32(vshrq_n_u32(riceParamPart128, 1), vaddq_u32(drflac__vnotq_u32(vandq_u32(riceParamPart128, one128)), one128)); + + for (i = 0; i < 4; i += 1) { + int64x1_t prediction64; + + prediction128 = veorq_s64(prediction128, prediction128); /* Reset to 0. */ + switch (order) + { + case 12: + case 11: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_low_s32(coefficients128_8), vget_low_s32(samples128_8))); + case 10: + case 9: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_high_s32(coefficients128_8), vget_high_s32(samples128_8))); + case 8: + case 7: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_low_s32(coefficients128_4), vget_low_s32(samples128_4))); + case 6: + case 5: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_high_s32(coefficients128_4), vget_high_s32(samples128_4))); + case 4: + case 3: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_low_s32(coefficients128_0), vget_low_s32(samples128_0))); + case 2: + case 1: prediction128 = vaddq_s64(prediction128, vmull_s32(vget_high_s32(coefficients128_0), vget_high_s32(samples128_0))); + } + + /* Horizontal add and shift. */ + prediction64 = drflac__vhaddq_s64(prediction128); + prediction64 = vshl_s64(prediction64, shift64); + prediction64 = vadd_s64(prediction64, vdup_n_s64(vgetq_lane_u32(riceParamPart128, 0))); + + /* Our value should be sitting in prediction64[0]. We need to combine this with our SSE samples. */ + samples128_8 = drflac__valignrq_s32_1(samples128_4, samples128_8); + samples128_4 = drflac__valignrq_s32_1(samples128_0, samples128_4); + samples128_0 = drflac__valignrq_s32_1(vcombine_s32(vreinterpret_s32_s64(prediction64), vdup_n_s32(0)), samples128_0); + + /* Slide our rice parameter down so that the value in position 0 contains the next one to process. */ + riceParamPart128 = drflac__valignrq_u32_1(vdupq_n_u32(0), riceParamPart128); + } + + /* We store samples in groups of 4. */ + vst1q_s32(pDecodedSamples, samples128_0); + pDecodedSamples += 4; + } + + /* Make sure we process the last few samples. */ + i = (count & ~3); + while (i < (int)count) { + /* Rice extraction. */ + if (!drflac__read_rice_parts_x1(bs, riceParam, &zeroCountParts[0], &riceParamParts[0])) { + return DRFLAC_FALSE; + } + + /* Rice reconstruction. */ + riceParamParts[0] &= riceParamMask; + riceParamParts[0] |= (zeroCountParts[0] << riceParam); + riceParamParts[0] = (riceParamParts[0] >> 1) ^ t[riceParamParts[0] & 0x01]; + + /* Sample reconstruction. */ + pDecodedSamples[0] = riceParamParts[0] + drflac__calculate_prediction_64(order, shift, coefficients, pDecodedSamples); + + i += 1; + pDecodedSamples += 1; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_samples_with_residual__rice__neon(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(pSamplesOut != NULL); + + /* In my testing the order is rarely > 12, so in this case I'm going to simplify the NEON implementation by only handling order <= 12. */ + if (lpcOrder > 0 && lpcOrder <= 12) { + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + return drflac__decode_samples_with_residual__rice__neon_64(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } else { + return drflac__decode_samples_with_residual__rice__neon_32(bs, count, riceParam, lpcOrder, lpcShift, coefficients, pSamplesOut); + } + } else { + return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + } +} +#endif + +static drflac_bool32 drflac__decode_samples_with_residual__rice(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 riceParam, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ +#if defined(DRFLAC_SUPPORT_SSE41) + if (drflac__gIsSSE41Supported) { + return drflac__decode_samples_with_residual__rice__sse41(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported) { + return drflac__decode_samples_with_residual__rice__neon(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + } else +#endif + { + /* Scalar fallback. */ + #if 0 + return drflac__decode_samples_with_residual__rice__reference(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + #else + return drflac__decode_samples_with_residual__rice__scalar(bs, bitsPerSample, count, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pSamplesOut); + #endif + } +} + +/* Reads and seeks past a string of residual values as Rice codes. The decoder should be sitting on the first bit of the Rice codes. */ +static drflac_bool32 drflac__read_and_seek_residual__rice(drflac_bs* bs, drflac_uint32 count, drflac_uint8 riceParam) +{ + drflac_uint32 i; + + DRFLAC_ASSERT(bs != NULL); + + for (i = 0; i < count; ++i) { + if (!drflac__seek_rice_parts(bs, riceParam)) { + return DRFLAC_FALSE; + } + } + + return DRFLAC_TRUE; +} + +#if defined(__clang__) +__attribute__((no_sanitize("signed-integer-overflow"))) +#endif +static drflac_bool32 drflac__decode_samples_with_residual__unencoded(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 count, drflac_uint8 unencodedBitsPerSample, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pSamplesOut) +{ + drflac_uint32 i; + + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(unencodedBitsPerSample <= 31); /* <-- unencodedBitsPerSample is a 5 bit number, so cannot exceed 31. */ + DRFLAC_ASSERT(pSamplesOut != NULL); + + for (i = 0; i < count; ++i) { + if (unencodedBitsPerSample > 0) { + if (!drflac__read_int32(bs, unencodedBitsPerSample, pSamplesOut + i)) { + return DRFLAC_FALSE; + } + } else { + pSamplesOut[i] = 0; + } + + if (drflac__use_64_bit_prediction(bitsPerSample, lpcOrder, lpcPrecision)) { + pSamplesOut[i] += drflac__calculate_prediction_64(lpcOrder, lpcShift, coefficients, pSamplesOut + i); + } else { + pSamplesOut[i] += drflac__calculate_prediction_32(lpcOrder, lpcShift, coefficients, pSamplesOut + i); + } + } + + return DRFLAC_TRUE; } -// Reads and decodes the residual for the sub-frame the decoder is currently sitting on. This function should be called -// when the decoder is sitting at the very start of the RESIDUAL block. The first residuals will be ignored. The -// and parameters are used to determine how many residual values need to be decoded. -static bool drflac__decode_samples_with_residual(drflac* pFlac, unsigned int blockSize, unsigned int order, int shift, const short* coefficients, int* pDecodedSamples) +/* +Reads and decodes the residual for the sub-frame the decoder is currently sitting on. This function should be called +when the decoder is sitting at the very start of the RESIDUAL block. The first residuals will be ignored. The + and parameters are used to determine how many residual values need to be decoded. +*/ +static drflac_bool32 drflac__decode_samples_with_residual(drflac_bs* bs, drflac_uint32 bitsPerSample, drflac_uint32 blockSize, drflac_uint32 lpcOrder, drflac_int32 lpcShift, drflac_uint32 lpcPrecision, const drflac_int32* coefficients, drflac_int32* pDecodedSamples) { - assert(pFlac != NULL); - assert(blockSize != 0); - assert(pDecodedSamples != NULL); // <-- Should we allow NULL, in which case we just seek past the residual rather than do a full decode? + drflac_uint8 residualMethod; + drflac_uint8 partitionOrder; + drflac_uint32 samplesInPartition; + drflac_uint32 partitionsRemaining; - unsigned char residualMethod; - if (!drflac__read_uint8(pFlac, 2, &residualMethod)) { - return false; + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(blockSize != 0); + DRFLAC_ASSERT(pDecodedSamples != NULL); /* <-- Should we allow NULL, in which case we just seek past the residual rather than do a full decode? */ + + if (!drflac__read_uint8(bs, 2, &residualMethod)) { + return DRFLAC_FALSE; } if (residualMethod != DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE && residualMethod != DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE2) { - return false; // Unknown or unsupported residual coding method. + return DRFLAC_FALSE; /* Unknown or unsupported residual coding method. */ } - // Ignore the first values. - pDecodedSamples += order; + /* Ignore the first values. */ + pDecodedSamples += lpcOrder; - - unsigned char partitionOrder; - if (!drflac__read_uint8(pFlac, 4, &partitionOrder)) { - return false; + if (!drflac__read_uint8(bs, 4, &partitionOrder)) { + return DRFLAC_FALSE; } + /* + From the FLAC spec: + The Rice partition order in a Rice-coded residual section must be less than or equal to 8. + */ + if (partitionOrder > 8) { + return DRFLAC_FALSE; + } - unsigned int samplesInPartition = (blockSize / (1 << partitionOrder)) - order; - unsigned int partitionsRemaining = (1 << partitionOrder); - for (;;) - { - unsigned char riceParam = 0; + /* Validation check. */ + if ((blockSize / (1 << partitionOrder)) < lpcOrder) { + return DRFLAC_FALSE; + } + + samplesInPartition = (blockSize / (1 << partitionOrder)) - lpcOrder; + partitionsRemaining = (1 << partitionOrder); + for (;;) { + drflac_uint8 riceParam = 0; if (residualMethod == DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE) { - if (!drflac__read_uint8(pFlac, 4, &riceParam)) { - return false; + if (!drflac__read_uint8(bs, 4, &riceParam)) { + return DRFLAC_FALSE; } - if (riceParam == 16) { + if (riceParam == 15) { riceParam = 0xFF; } } else if (residualMethod == DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE2) { - if (!drflac__read_uint8(pFlac, 5, &riceParam)) { - return false; + if (!drflac__read_uint8(bs, 5, &riceParam)) { + return DRFLAC_FALSE; } - if (riceParam == 32) { + if (riceParam == 31) { riceParam = 0xFF; } } if (riceParam != 0xFF) { - if (!drflac__decode_samples_with_residual__rice(pFlac, samplesInPartition, riceParam, order, shift, coefficients, pDecodedSamples)) { - return false; + if (!drflac__decode_samples_with_residual__rice(bs, bitsPerSample, samplesInPartition, riceParam, lpcOrder, lpcShift, lpcPrecision, coefficients, pDecodedSamples)) { + return DRFLAC_FALSE; } } else { - unsigned char unencodedBitsPerSample = 0; - if (!drflac__read_uint8(pFlac, 5, &unencodedBitsPerSample)) { - return false; + drflac_uint8 unencodedBitsPerSample = 0; + if (!drflac__read_uint8(bs, 5, &unencodedBitsPerSample)) { + return DRFLAC_FALSE; } - if (!drflac__decode_samples_with_residual__unencoded(pFlac, samplesInPartition, unencodedBitsPerSample, order, shift, coefficients, pDecodedSamples)) { - return false; + if (!drflac__decode_samples_with_residual__unencoded(bs, bitsPerSample, samplesInPartition, unencodedBitsPerSample, lpcOrder, lpcShift, lpcPrecision, coefficients, pDecodedSamples)) { + return DRFLAC_FALSE; } } pDecodedSamples += samplesInPartition; - if (partitionsRemaining == 1) { break; } partitionsRemaining -= 1; - samplesInPartition = blockSize / (1 << partitionOrder); + + if (partitionOrder != 0) { + samplesInPartition = blockSize / (1 << partitionOrder); + } } - return true; + return DRFLAC_TRUE; } -// Reads and seeks past the residual for the sub-frame the decoder is currently sitting on. This function should be called -// when the decoder is sitting at the very start of the RESIDUAL block. The first residuals will be set to 0. The -// and parameters are used to determine how many residual values need to be decoded. -static bool drflac__read_and_seek_residual(drflac* pFlac, unsigned int blockSize, unsigned int order) +/* +Reads and seeks past the residual for the sub-frame the decoder is currently sitting on. This function should be called +when the decoder is sitting at the very start of the RESIDUAL block. The first residuals will be set to 0. The + and parameters are used to determine how many residual values need to be decoded. +*/ +static drflac_bool32 drflac__read_and_seek_residual(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 order) { - assert(pFlac != NULL); - assert(blockSize != 0); + drflac_uint8 residualMethod; + drflac_uint8 partitionOrder; + drflac_uint32 samplesInPartition; + drflac_uint32 partitionsRemaining; - unsigned char residualMethod; - if (!drflac__read_uint8(pFlac, 2, &residualMethod)) { - return false; + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(blockSize != 0); + + if (!drflac__read_uint8(bs, 2, &residualMethod)) { + return DRFLAC_FALSE; } if (residualMethod != DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE && residualMethod != DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE2) { - return false; // Unknown or unsupported residual coding method. + return DRFLAC_FALSE; /* Unknown or unsupported residual coding method. */ } - unsigned char partitionOrder; - if (!drflac__read_uint8(pFlac, 4, &partitionOrder)) { - return false; + if (!drflac__read_uint8(bs, 4, &partitionOrder)) { + return DRFLAC_FALSE; } - unsigned int samplesInPartition = (blockSize / (1 << partitionOrder)) - order; - unsigned int partitionsRemaining = (1 << partitionOrder); + /* + From the FLAC spec: + The Rice partition order in a Rice-coded residual section must be less than or equal to 8. + */ + if (partitionOrder > 8) { + return DRFLAC_FALSE; + } + + /* Validation check. */ + if ((blockSize / (1 << partitionOrder)) <= order) { + return DRFLAC_FALSE; + } + + samplesInPartition = (blockSize / (1 << partitionOrder)) - order; + partitionsRemaining = (1 << partitionOrder); for (;;) { - unsigned char riceParam = 0; + drflac_uint8 riceParam = 0; if (residualMethod == DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE) { - if (!drflac__read_uint8(pFlac, 4, &riceParam)) { - return false; + if (!drflac__read_uint8(bs, 4, &riceParam)) { + return DRFLAC_FALSE; } - if (riceParam == 16) { + if (riceParam == 15) { riceParam = 0xFF; } } else if (residualMethod == DRFLAC_RESIDUAL_CODING_METHOD_PARTITIONED_RICE2) { - if (!drflac__read_uint8(pFlac, 5, &riceParam)) { - return false; + if (!drflac__read_uint8(bs, 5, &riceParam)) { + return DRFLAC_FALSE; } - if (riceParam == 32) { + if (riceParam == 31) { riceParam = 0xFF; } } if (riceParam != 0xFF) { - if (!drflac__read_and_seek_residual__rice(pFlac, samplesInPartition, riceParam)) { - return false; + if (!drflac__read_and_seek_residual__rice(bs, samplesInPartition, riceParam)) { + return DRFLAC_FALSE; } } else { - unsigned char unencodedBitsPerSample = 0; - if (!drflac__read_uint8(pFlac, 5, &unencodedBitsPerSample)) { - return false; + drflac_uint8 unencodedBitsPerSample = 0; + if (!drflac__read_uint8(bs, 5, &unencodedBitsPerSample)) { + return DRFLAC_FALSE; } - if (!drflac__seek_bits(pFlac, unencodedBitsPerSample * samplesInPartition)) { - return false; + if (!drflac__seek_bits(bs, unencodedBitsPerSample * samplesInPartition)) { + return DRFLAC_FALSE; } } @@ -1748,44 +5000,52 @@ static bool drflac__read_and_seek_residual(drflac* pFlac, unsigned int blockSize samplesInPartition = blockSize / (1 << partitionOrder); } - return true; + return DRFLAC_TRUE; } -static bool drflac__decode_samples__constant(drflac* pFlac, drflac_subframe* pSubframe) +static drflac_bool32 drflac__decode_samples__constant(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 subframeBitsPerSample, drflac_int32* pDecodedSamples) { - // Only a single sample needs to be decoded here. - int sample; - if (!drflac__read_int32(pFlac, pSubframe->bitsPerSample, &sample)) { - return false; + drflac_uint32 i; + + /* Only a single sample needs to be decoded here. */ + drflac_int32 sample; + if (!drflac__read_int32(bs, subframeBitsPerSample, &sample)) { + return DRFLAC_FALSE; } - // We don't really need to expand this, but it does simplify the process of reading samples. If this becomes a performance issue (unlikely) - // we'll want to look at a more efficient way. - for (unsigned int i = 0; i < pFlac->currentFrame.blockSize; ++i) { - pSubframe->pDecodedSamples[i] = sample; + /* + We don't really need to expand this, but it does simplify the process of reading samples. If this becomes a performance issue (unlikely) + we'll want to look at a more efficient way. + */ + for (i = 0; i < blockSize; ++i) { + pDecodedSamples[i] = sample; } - return true; + return DRFLAC_TRUE; } -static bool drflac__decode_samples__verbatim(drflac* pFlac, drflac_subframe* pSubframe) +static drflac_bool32 drflac__decode_samples__verbatim(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 subframeBitsPerSample, drflac_int32* pDecodedSamples) { - for (unsigned int i = 0; i < pFlac->currentFrame.blockSize; ++i) { - int sample; - if (!drflac__read_int32(pFlac, pSubframe->bitsPerSample, &sample)) { - return false; + drflac_uint32 i; + + for (i = 0; i < blockSize; ++i) { + drflac_int32 sample; + if (!drflac__read_int32(bs, subframeBitsPerSample, &sample)) { + return DRFLAC_FALSE; } - pSubframe->pDecodedSamples[i] = sample; + pDecodedSamples[i] = sample; } - return true; + return DRFLAC_TRUE; } -static bool drflac__decode_samples__fixed(drflac* pFlac, drflac_subframe* pSubframe) +static drflac_bool32 drflac__decode_samples__fixed(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 subframeBitsPerSample, drflac_uint8 lpcOrder, drflac_int32* pDecodedSamples) { - short lpcCoefficientsTable[5][4] = { + drflac_uint32 i; + + static drflac_int32 lpcCoefficientsTable[5][4] = { {0, 0, 0, 0}, {1, 0, 0, 0}, {2, -1, 0, 0}, @@ -1793,210 +5053,279 @@ static bool drflac__decode_samples__fixed(drflac* pFlac, drflac_subframe* pSubfr {4, -6, 4, -1} }; - // Warm up samples and coefficients. - for (unsigned int i = 0; i < pSubframe->lpcOrder; ++i) { - int sample; - if (!drflac__read_int32(pFlac, pSubframe->bitsPerSample, &sample)) { - return false; + /* Warm up samples and coefficients. */ + for (i = 0; i < lpcOrder; ++i) { + drflac_int32 sample; + if (!drflac__read_int32(bs, subframeBitsPerSample, &sample)) { + return DRFLAC_FALSE; } - pSubframe->pDecodedSamples[i] = sample; + pDecodedSamples[i] = sample; } - - if (!drflac__decode_samples_with_residual(pFlac, pFlac->currentFrame.blockSize, pSubframe->lpcOrder, 0, lpcCoefficientsTable[pSubframe->lpcOrder], pSubframe->pDecodedSamples)) { - return false; + if (!drflac__decode_samples_with_residual(bs, subframeBitsPerSample, blockSize, lpcOrder, 0, 4, lpcCoefficientsTable[lpcOrder], pDecodedSamples)) { + return DRFLAC_FALSE; } - return true; + return DRFLAC_TRUE; } -static bool drflac__decode_samples__lpc(drflac* pFlac, drflac_subframe* pSubframe) +static drflac_bool32 drflac__decode_samples__lpc(drflac_bs* bs, drflac_uint32 blockSize, drflac_uint32 bitsPerSample, drflac_uint8 lpcOrder, drflac_int32* pDecodedSamples) { - // Warm up samples. - for (unsigned int i = 0; i < pSubframe->lpcOrder; ++i) { - int sample; - if (!drflac__read_int32(pFlac, pSubframe->bitsPerSample, &sample)) { - return false; + drflac_uint8 i; + drflac_uint8 lpcPrecision; + drflac_int8 lpcShift; + drflac_int32 coefficients[32]; + + /* Warm up samples. */ + for (i = 0; i < lpcOrder; ++i) { + drflac_int32 sample; + if (!drflac__read_int32(bs, bitsPerSample, &sample)) { + return DRFLAC_FALSE; } - pSubframe->pDecodedSamples[i] = sample; + pDecodedSamples[i] = sample; } - unsigned char lpcPrecision; - if (!drflac__read_uint8(pFlac, 4, &lpcPrecision)) { - return false; + if (!drflac__read_uint8(bs, 4, &lpcPrecision)) { + return DRFLAC_FALSE; } if (lpcPrecision == 15) { - return false; // Invalid. + return DRFLAC_FALSE; /* Invalid. */ } lpcPrecision += 1; - - signed char lpcShift; - if (!drflac__read_int8(pFlac, 5, &lpcShift)) { - return false; + if (!drflac__read_int8(bs, 5, &lpcShift)) { + return DRFLAC_FALSE; } + /* + From the FLAC specification: - short coefficients[32]; - for (unsigned int i = 0; i < pSubframe->lpcOrder; ++i) { - if (!drflac__read_int16(pFlac, lpcPrecision, coefficients + i)) { - return false; + Quantized linear predictor coefficient shift needed in bits (NOTE: this number is signed two's-complement) + + Emphasis on the "signed two's-complement". In practice there does not seem to be any encoders nor decoders supporting negative shifts. For now dr_flac is + not going to support negative shifts as I don't have any reference files. However, when a reference file comes through I will consider adding support. + */ + if (lpcShift < 0) { + return DRFLAC_FALSE; + } + + DRFLAC_ZERO_MEMORY(coefficients, sizeof(coefficients)); + for (i = 0; i < lpcOrder; ++i) { + if (!drflac__read_int32(bs, lpcPrecision, coefficients + i)) { + return DRFLAC_FALSE; } } - if (!drflac__decode_samples_with_residual(pFlac, pFlac->currentFrame.blockSize, pSubframe->lpcOrder, lpcShift, coefficients, pSubframe->pDecodedSamples)) { - return false; + if (!drflac__decode_samples_with_residual(bs, bitsPerSample, blockSize, lpcOrder, lpcShift, lpcPrecision, coefficients, pDecodedSamples)) { + return DRFLAC_FALSE; } - return true; + return DRFLAC_TRUE; } -static bool drflac__read_next_frame_header(drflac* pFlac) +static drflac_bool32 drflac__read_next_flac_frame_header(drflac_bs* bs, drflac_uint8 streaminfoBitsPerSample, drflac_frame_header* header) { - assert(pFlac != NULL); - assert(pFlac->onRead != NULL); + const drflac_uint32 sampleRateTable[12] = {0, 88200, 176400, 192000, 8000, 16000, 22050, 24000, 32000, 44100, 48000, 96000}; + const drflac_uint8 bitsPerSampleTable[8] = {0, 8, 12, (drflac_uint8)-1, 16, 20, 24, (drflac_uint8)-1}; /* -1 = reserved. */ - // At the moment the sync code is as a form of basic validation. The CRC is stored, but is unused at the moment. This - // should probably be handled better in the future. + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(header != NULL); - const int sampleRateTable[12] = {0, 88200, 176400, 192000, 8000, 16000, 22050, 24000, 32000, 44100, 48000, 96000}; - const uint8_t bitsPerSampleTable[8] = {0, 8, 12, (uint8_t)-1, 16, 20, 24, (uint8_t)-1}; // -1 = reserved. + /* Keep looping until we find a valid sync code. */ + for (;;) { + drflac_uint8 crc8 = 0xCE; /* 0xCE = drflac_crc8(0, 0x3FFE, 14); */ + drflac_uint8 reserved = 0; + drflac_uint8 blockingStrategy = 0; + drflac_uint8 blockSize = 0; + drflac_uint8 sampleRate = 0; + drflac_uint8 channelAssignment = 0; + drflac_uint8 bitsPerSample = 0; + drflac_bool32 isVariableBlockSize; - unsigned short syncCode = 0; - if (!drflac__read_uint16(pFlac, 14, &syncCode)) { - return false; - } - - if (syncCode != 0x3FFE) { - // TODO: Try and recover by attempting to seek to and read the next frame? - return false; - } - - unsigned char reserved; - if (!drflac__read_uint8(pFlac, 1, &reserved)) { - return false; - } - - unsigned char blockingStrategy = 0; - if (!drflac__read_uint8(pFlac, 1, &blockingStrategy)) { - return false; - } - - - - unsigned char blockSize = 0; - if (!drflac__read_uint8(pFlac, 4, &blockSize)) { - return false; - } - - unsigned char sampleRate = 0; - if (!drflac__read_uint8(pFlac, 4, &sampleRate)) { - return false; - } - - unsigned char channelAssignment = 0; - if (!drflac__read_uint8(pFlac, 4, &channelAssignment)) { - return false; - } - - unsigned char bitsPerSample = 0; - if (!drflac__read_uint8(pFlac, 3, &bitsPerSample)) { - return false; - } - - if (!drflac__read_uint8(pFlac, 1, &reserved)) { - return false; - } - - - unsigned char isVariableBlockSize = blockingStrategy == 1; - if (isVariableBlockSize) { - pFlac->currentFrame.frameNumber = 0; - if (!drflac__read_utf8_coded_number(pFlac, &pFlac->currentFrame.sampleNumber)) { - return false; + if (!drflac__find_and_seek_to_next_sync_code(bs)) { + return DRFLAC_FALSE; } - } else { - unsigned long long frameNumber = 0; - if (!drflac__read_utf8_coded_number(pFlac, &frameNumber)) { - return false; + + if (!drflac__read_uint8(bs, 1, &reserved)) { + return DRFLAC_FALSE; } - pFlac->currentFrame.frameNumber = (unsigned int)frameNumber; // <-- Safe cast. - pFlac->currentFrame.sampleNumber = 0; + if (reserved == 1) { + continue; + } + crc8 = drflac_crc8(crc8, reserved, 1); + + if (!drflac__read_uint8(bs, 1, &blockingStrategy)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, blockingStrategy, 1); + + if (!drflac__read_uint8(bs, 4, &blockSize)) { + return DRFLAC_FALSE; + } + if (blockSize == 0) { + continue; + } + crc8 = drflac_crc8(crc8, blockSize, 4); + + if (!drflac__read_uint8(bs, 4, &sampleRate)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, sampleRate, 4); + + if (!drflac__read_uint8(bs, 4, &channelAssignment)) { + return DRFLAC_FALSE; + } + if (channelAssignment > 10) { + continue; + } + crc8 = drflac_crc8(crc8, channelAssignment, 4); + + if (!drflac__read_uint8(bs, 3, &bitsPerSample)) { + return DRFLAC_FALSE; + } + if (bitsPerSample == 3 || bitsPerSample == 7) { + continue; + } + crc8 = drflac_crc8(crc8, bitsPerSample, 3); + + + if (!drflac__read_uint8(bs, 1, &reserved)) { + return DRFLAC_FALSE; + } + if (reserved == 1) { + continue; + } + crc8 = drflac_crc8(crc8, reserved, 1); + + + isVariableBlockSize = blockingStrategy == 1; + if (isVariableBlockSize) { + drflac_uint64 pcmFrameNumber; + drflac_result result = drflac__read_utf8_coded_number(bs, &pcmFrameNumber, &crc8); + if (result != DRFLAC_SUCCESS) { + if (result == DRFLAC_AT_END) { + return DRFLAC_FALSE; + } else { + continue; + } + } + header->flacFrameNumber = 0; + header->pcmFrameNumber = pcmFrameNumber; + } else { + drflac_uint64 flacFrameNumber = 0; + drflac_result result = drflac__read_utf8_coded_number(bs, &flacFrameNumber, &crc8); + if (result != DRFLAC_SUCCESS) { + if (result == DRFLAC_AT_END) { + return DRFLAC_FALSE; + } else { + continue; + } + } + header->flacFrameNumber = (drflac_uint32)flacFrameNumber; /* <-- Safe cast. */ + header->pcmFrameNumber = 0; + } + + + DRFLAC_ASSERT(blockSize > 0); + if (blockSize == 1) { + header->blockSizeInPCMFrames = 192; + } else if (blockSize <= 5) { + DRFLAC_ASSERT(blockSize >= 2); + header->blockSizeInPCMFrames = 576 * (1 << (blockSize - 2)); + } else if (blockSize == 6) { + if (!drflac__read_uint16(bs, 8, &header->blockSizeInPCMFrames)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->blockSizeInPCMFrames, 8); + header->blockSizeInPCMFrames += 1; + } else if (blockSize == 7) { + if (!drflac__read_uint16(bs, 16, &header->blockSizeInPCMFrames)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->blockSizeInPCMFrames, 16); + if (header->blockSizeInPCMFrames == 0xFFFF) { + return DRFLAC_FALSE; /* Frame is too big. This is the size of the frame minus 1. The STREAMINFO block defines the max block size which is 16-bits. Adding one will make it 17 bits and therefore too big. */ + } + header->blockSizeInPCMFrames += 1; + } else { + DRFLAC_ASSERT(blockSize >= 8); + header->blockSizeInPCMFrames = 256 * (1 << (blockSize - 8)); + } + + + if (sampleRate <= 11) { + header->sampleRate = sampleRateTable[sampleRate]; + } else if (sampleRate == 12) { + if (!drflac__read_uint32(bs, 8, &header->sampleRate)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->sampleRate, 8); + header->sampleRate *= 1000; + } else if (sampleRate == 13) { + if (!drflac__read_uint32(bs, 16, &header->sampleRate)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->sampleRate, 16); + } else if (sampleRate == 14) { + if (!drflac__read_uint32(bs, 16, &header->sampleRate)) { + return DRFLAC_FALSE; + } + crc8 = drflac_crc8(crc8, header->sampleRate, 16); + header->sampleRate *= 10; + } else { + continue; /* Invalid. Assume an invalid block. */ + } + + + header->channelAssignment = channelAssignment; + + header->bitsPerSample = bitsPerSampleTable[bitsPerSample]; + if (header->bitsPerSample == 0) { + header->bitsPerSample = streaminfoBitsPerSample; + } + + if (header->bitsPerSample != streaminfoBitsPerSample) { + /* If this subframe has a different bitsPerSample then streaminfo or the first frame, reject it */ + return DRFLAC_FALSE; + } + + if (!drflac__read_uint8(bs, 8, &header->crc8)) { + return DRFLAC_FALSE; + } + +#ifndef DR_FLAC_NO_CRC + if (header->crc8 != crc8) { + continue; /* CRC mismatch. Loop back to the top and find the next sync code. */ + } +#endif + return DRFLAC_TRUE; } - - - if (blockSize == 1) { - pFlac->currentFrame.blockSize = 192; - } else if (blockSize >= 2 && blockSize <= 5) { - pFlac->currentFrame.blockSize = 576 * (1 << (blockSize - 2)); - } else if (blockSize == 6) { - if (!drflac__read_uint16(pFlac, 8, &pFlac->currentFrame.blockSize)) { - return false; - } - pFlac->currentFrame.blockSize += 1; - } else if (blockSize == 7) { - if (!drflac__read_uint16(pFlac, 16, &pFlac->currentFrame.blockSize)) { - return false; - } - pFlac->currentFrame.blockSize += 1; - } else { - pFlac->currentFrame.blockSize = 256 * (1 << (blockSize - 8)); - } - - - if (sampleRate <= 11) { - pFlac->currentFrame.sampleRate = sampleRateTable[sampleRate]; - } else if (sampleRate == 12) { - if (!drflac__read_uint32(pFlac, 8, &pFlac->currentFrame.sampleRate)) { - return false; - } - pFlac->currentFrame.sampleRate *= 1000; - } else if (sampleRate == 13) { - if (!drflac__read_uint32(pFlac, 16, &pFlac->currentFrame.sampleRate)) { - return false; - } - } else if (sampleRate == 14) { - if (!drflac__read_uint32(pFlac, 16, &pFlac->currentFrame.sampleRate)) { - return false; - } - pFlac->currentFrame.sampleRate *= 10; - } else { - return false; // Invalid. - } - - - pFlac->currentFrame.channelAssignment = channelAssignment; - - pFlac->currentFrame.bitsPerSample = bitsPerSampleTable[bitsPerSample]; - if (pFlac->currentFrame.bitsPerSample == 0) { - pFlac->currentFrame.bitsPerSample = pFlac->bitsPerSample; - } - - if (drflac__read_uint8(pFlac, 8, &pFlac->currentFrame.crc8) != 1) { - return false; - } - - memset(pFlac->currentFrame.subframes, 0, sizeof(pFlac->currentFrame.subframes)); - - return true; } -static bool drflac__read_subframe_header(drflac* pFlac, drflac_subframe* pSubframe) +static drflac_bool32 drflac__read_subframe_header(drflac_bs* bs, drflac_subframe* pSubframe) { - unsigned char header; - if (!drflac__read_uint8(pFlac, 8, &header)) { - return false; + drflac_uint8 header; + int type; + + if (!drflac__read_uint8(bs, 8, &header)) { + return DRFLAC_FALSE; } - // First bit should always be 0. + /* First bit should always be 0. */ if ((header & 0x80) != 0) { - return false; + return DRFLAC_FALSE; } - int type = (header & 0x7E) >> 1; + /* + Default to 0 for the LPC order. It's important that we always set this to 0 for non LPC + and FIXED subframes because we'll be using it in a generic validation check later. + */ + pSubframe->lpcOrder = 0; + + type = (header & 0x7E) >> 1; if (type == 0) { pSubframe->subframeType = DRFLAC_SUBFRAME_CONSTANT; } else if (type == 1) { @@ -2004,10 +5333,10 @@ static bool drflac__read_subframe_header(drflac* pFlac, drflac_subframe* pSubfra } else { if ((type & 0x20) != 0) { pSubframe->subframeType = DRFLAC_SUBFRAME_LPC; - pSubframe->lpcOrder = (type & 0x1F) + 1; + pSubframe->lpcOrder = (drflac_uint8)(type & 0x1F) + 1; } else if ((type & 0x08) != 0) { pSubframe->subframeType = DRFLAC_SUBFRAME_FIXED; - pSubframe->lpcOrder = (type & 0x07); + pSubframe->lpcOrder = (drflac_uint8)(type & 0x07); if (pSubframe->lpcOrder > 4) { pSubframe->subframeType = DRFLAC_SUBFRAME_RESERVED; pSubframe->lpcOrder = 0; @@ -2018,961 +5347,7308 @@ static bool drflac__read_subframe_header(drflac* pFlac, drflac_subframe* pSubfra } if (pSubframe->subframeType == DRFLAC_SUBFRAME_RESERVED) { - return false; + return DRFLAC_FALSE; } - // Wasted bits per sample. + /* Wasted bits per sample. */ pSubframe->wastedBitsPerSample = 0; if ((header & 0x01) == 1) { unsigned int wastedBitsPerSample; - if (!drflac__seek_past_next_set_bit(pFlac, &wastedBitsPerSample)) { - return false; + if (!drflac__seek_past_next_set_bit(bs, &wastedBitsPerSample)) { + return DRFLAC_FALSE; } - pSubframe->wastedBitsPerSample = (unsigned char)wastedBitsPerSample + 1; + pSubframe->wastedBitsPerSample = (drflac_uint8)wastedBitsPerSample + 1; } - return true; + return DRFLAC_TRUE; } -static bool drflac__decode_subframe(drflac* pFlac, int subframeIndex) +static drflac_bool32 drflac__decode_subframe(drflac_bs* bs, drflac_frame* frame, int subframeIndex, drflac_int32* pDecodedSamplesOut) { - assert(pFlac != NULL); + drflac_subframe* pSubframe; + drflac_uint32 subframeBitsPerSample; - drflac_subframe* pSubframe = pFlac->currentFrame.subframes + subframeIndex; - if (!drflac__read_subframe_header(pFlac, pSubframe)) { - return false; + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(frame != NULL); + + pSubframe = frame->subframes + subframeIndex; + if (!drflac__read_subframe_header(bs, pSubframe)) { + return DRFLAC_FALSE; } - // Side channels require an extra bit per sample. Took a while to figure that one out... - pSubframe->bitsPerSample = pFlac->currentFrame.bitsPerSample; - if ((pFlac->currentFrame.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE || pFlac->currentFrame.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE) && subframeIndex == 1) { - pSubframe->bitsPerSample += 1; - } else if (pFlac->currentFrame.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE && subframeIndex == 0) { - pSubframe->bitsPerSample += 1; + /* Side channels require an extra bit per sample. Took a while to figure that one out... */ + subframeBitsPerSample = frame->header.bitsPerSample; + if ((frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE || frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE) && subframeIndex == 1) { + subframeBitsPerSample += 1; + } else if (frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE && subframeIndex == 0) { + subframeBitsPerSample += 1; } - // Need to handle wasted bits per sample. - pSubframe->bitsPerSample -= pSubframe->wastedBitsPerSample; - pSubframe->pDecodedSamples = pFlac->pDecodedSamples + (pFlac->currentFrame.blockSize * subframeIndex); + if (subframeBitsPerSample > 32) { + /* libFLAC and ffmpeg reject 33-bit subframes as well */ + return DRFLAC_FALSE; + } + + /* Need to handle wasted bits per sample. */ + if (pSubframe->wastedBitsPerSample >= subframeBitsPerSample) { + return DRFLAC_FALSE; + } + subframeBitsPerSample -= pSubframe->wastedBitsPerSample; + + pSubframe->pSamplesS32 = pDecodedSamplesOut; + + /* + pDecodedSamplesOut will be pointing to a buffer that was allocated with enough memory to store + maxBlockSizeInPCMFrames samples (as specified in the FLAC header). We need to guard against an + overflow here. At a higher level we are checking maxBlockSizeInPCMFrames from the header, but + here we need to do an additional check to ensure this frame's block size fully encompasses any + warmup samples which is determined by the LPC order. For non LPC and FIXED subframes, the LPC + order will be have been set to 0 in drflac__read_subframe_header(). + */ + if (frame->header.blockSizeInPCMFrames < pSubframe->lpcOrder) { + return DRFLAC_FALSE; + } switch (pSubframe->subframeType) { case DRFLAC_SUBFRAME_CONSTANT: { - drflac__decode_samples__constant(pFlac, pSubframe); + drflac__decode_samples__constant(bs, frame->header.blockSizeInPCMFrames, subframeBitsPerSample, pSubframe->pSamplesS32); } break; case DRFLAC_SUBFRAME_VERBATIM: { - drflac__decode_samples__verbatim(pFlac, pSubframe); + drflac__decode_samples__verbatim(bs, frame->header.blockSizeInPCMFrames, subframeBitsPerSample, pSubframe->pSamplesS32); } break; case DRFLAC_SUBFRAME_FIXED: { - drflac__decode_samples__fixed(pFlac, pSubframe); + drflac__decode_samples__fixed(bs, frame->header.blockSizeInPCMFrames, subframeBitsPerSample, pSubframe->lpcOrder, pSubframe->pSamplesS32); } break; case DRFLAC_SUBFRAME_LPC: { - drflac__decode_samples__lpc(pFlac, pSubframe); + drflac__decode_samples__lpc(bs, frame->header.blockSizeInPCMFrames, subframeBitsPerSample, pSubframe->lpcOrder, pSubframe->pSamplesS32); } break; - default: return false; + default: return DRFLAC_FALSE; } - return true; + return DRFLAC_TRUE; } -static bool drflac__seek_subframe(drflac* pFlac, int subframeIndex) +static drflac_bool32 drflac__seek_subframe(drflac_bs* bs, drflac_frame* frame, int subframeIndex) { - assert(pFlac != NULL); + drflac_subframe* pSubframe; + drflac_uint32 subframeBitsPerSample; - drflac_subframe* pSubframe = pFlac->currentFrame.subframes + subframeIndex; - if (!drflac__read_subframe_header(pFlac, pSubframe)) { - return false; + DRFLAC_ASSERT(bs != NULL); + DRFLAC_ASSERT(frame != NULL); + + pSubframe = frame->subframes + subframeIndex; + if (!drflac__read_subframe_header(bs, pSubframe)) { + return DRFLAC_FALSE; } - // Side channels require an extra bit per sample. Took a while to figure that one out... - pSubframe->bitsPerSample = pFlac->currentFrame.bitsPerSample; - if ((pFlac->currentFrame.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE || pFlac->currentFrame.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE) && subframeIndex == 1) { - pSubframe->bitsPerSample += 1; - } else if (pFlac->currentFrame.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE && subframeIndex == 0) { - pSubframe->bitsPerSample += 1; + /* Side channels require an extra bit per sample. Took a while to figure that one out... */ + subframeBitsPerSample = frame->header.bitsPerSample; + if ((frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE || frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE) && subframeIndex == 1) { + subframeBitsPerSample += 1; + } else if (frame->header.channelAssignment == DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE && subframeIndex == 0) { + subframeBitsPerSample += 1; } - // Need to handle wasted bits per sample. - pSubframe->bitsPerSample -= pSubframe->wastedBitsPerSample; - pSubframe->pDecodedSamples = pFlac->pDecodedSamples + (pFlac->currentFrame.blockSize * subframeIndex); + /* Need to handle wasted bits per sample. */ + if (pSubframe->wastedBitsPerSample >= subframeBitsPerSample) { + return DRFLAC_FALSE; + } + subframeBitsPerSample -= pSubframe->wastedBitsPerSample; + + pSubframe->pSamplesS32 = NULL; switch (pSubframe->subframeType) { case DRFLAC_SUBFRAME_CONSTANT: { - if (!drflac__seek_bits(pFlac, pSubframe->bitsPerSample)) { - return false; + if (!drflac__seek_bits(bs, subframeBitsPerSample)) { + return DRFLAC_FALSE; } } break; case DRFLAC_SUBFRAME_VERBATIM: { - unsigned int bitsToSeek = pFlac->currentFrame.blockSize * pSubframe->bitsPerSample; - if (!drflac__seek_bits(pFlac, bitsToSeek)) { - return false; + unsigned int bitsToSeek = frame->header.blockSizeInPCMFrames * subframeBitsPerSample; + if (!drflac__seek_bits(bs, bitsToSeek)) { + return DRFLAC_FALSE; } } break; case DRFLAC_SUBFRAME_FIXED: { - unsigned int bitsToSeek = pSubframe->lpcOrder * pSubframe->bitsPerSample; - if (!drflac__seek_bits(pFlac, bitsToSeek)) { - return false; + unsigned int bitsToSeek = pSubframe->lpcOrder * subframeBitsPerSample; + if (!drflac__seek_bits(bs, bitsToSeek)) { + return DRFLAC_FALSE; } - if (!drflac__read_and_seek_residual(pFlac, pFlac->currentFrame.blockSize, pSubframe->lpcOrder)) { - return false; + if (!drflac__read_and_seek_residual(bs, frame->header.blockSizeInPCMFrames, pSubframe->lpcOrder)) { + return DRFLAC_FALSE; } } break; case DRFLAC_SUBFRAME_LPC: { - unsigned int bitsToSeek = pSubframe->lpcOrder * pSubframe->bitsPerSample; - if (!drflac__seek_bits(pFlac, bitsToSeek)) { - return false; + drflac_uint8 lpcPrecision; + + unsigned int bitsToSeek = pSubframe->lpcOrder * subframeBitsPerSample; + if (!drflac__seek_bits(bs, bitsToSeek)) { + return DRFLAC_FALSE; } - unsigned char lpcPrecision; - if (!drflac__read_uint8(pFlac, 4, &lpcPrecision)) { - return false; + if (!drflac__read_uint8(bs, 4, &lpcPrecision)) { + return DRFLAC_FALSE; } if (lpcPrecision == 15) { - return false; // Invalid. + return DRFLAC_FALSE; /* Invalid. */ } lpcPrecision += 1; - bitsToSeek = (pSubframe->lpcOrder * lpcPrecision) + 5; // +5 for shift. - if (!drflac__seek_bits(pFlac, bitsToSeek)) { - return false; + bitsToSeek = (pSubframe->lpcOrder * lpcPrecision) + 5; /* +5 for shift. */ + if (!drflac__seek_bits(bs, bitsToSeek)) { + return DRFLAC_FALSE; } - if (!drflac__read_and_seek_residual(pFlac, pFlac->currentFrame.blockSize, pSubframe->lpcOrder)) { - return false; + if (!drflac__read_and_seek_residual(bs, frame->header.blockSizeInPCMFrames, pSubframe->lpcOrder)) { + return DRFLAC_FALSE; } } break; - default: return false; + default: return DRFLAC_FALSE; } - return true; + return DRFLAC_TRUE; } -static DRFLAC_INLINE int drflac__get_channel_count_from_channel_assignment(int channelAssignment) +static DRFLAC_INLINE drflac_uint8 drflac__get_channel_count_from_channel_assignment(drflac_int8 channelAssignment) { - assert(channelAssignment <= 10); + drflac_uint8 lookup[] = {1, 2, 3, 4, 5, 6, 7, 8, 2, 2, 2}; - int lookup[] = {1, 2, 3, 4, 5, 6, 7, 8, 2, 2, 2}; + DRFLAC_ASSERT(channelAssignment <= 10); return lookup[channelAssignment]; } -static bool drflac__decode_frame(drflac* pFlac) +static drflac_result drflac__decode_flac_frame(drflac* pFlac) { - // This function should be called while the stream is sitting on the first byte after the frame header. + int channelCount; + int i; + drflac_uint8 paddingSizeInBits; + drflac_uint16 desiredCRC16; +#ifndef DR_FLAC_NO_CRC + drflac_uint16 actualCRC16; +#endif - int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFrame.channelAssignment); - for (int i = 0; i < channelCount; ++i) - { - if (!drflac__decode_subframe(pFlac, i)) { - return false; + /* This function should be called while the stream is sitting on the first byte after the frame header. */ + DRFLAC_ZERO_MEMORY(pFlac->currentFLACFrame.subframes, sizeof(pFlac->currentFLACFrame.subframes)); + + /* The frame block size must never be larger than the maximum block size defined by the FLAC stream. */ + if (pFlac->currentFLACFrame.header.blockSizeInPCMFrames > pFlac->maxBlockSizeInPCMFrames) { + return DRFLAC_ERROR; + } + + /* The number of channels in the frame must match the channel count from the STREAMINFO block. */ + channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + if (channelCount != (int)pFlac->channels) { + return DRFLAC_ERROR; + } + + for (i = 0; i < channelCount; ++i) { + if (!drflac__decode_subframe(&pFlac->bs, &pFlac->currentFLACFrame, i, pFlac->pDecodedSamples + (pFlac->currentFLACFrame.header.blockSizeInPCMFrames * i))) { + return DRFLAC_ERROR; } } - // At the end of the frame sits the padding and CRC. We don't use these so we can just seek past. - if (!drflac__seek_bits(pFlac, (DRFLAC_CACHE_L1_BITS_REMAINING & 7) + 16)) { - return false; - } - - - pFlac->currentFrame.samplesRemaining = pFlac->currentFrame.blockSize * channelCount; - - return true; -} - -static bool drflac__seek_frame(drflac* pFlac) -{ - int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFrame.channelAssignment); - for (int i = 0; i < channelCount; ++i) - { - if (!drflac__seek_subframe(pFlac, i)) { - return false; + paddingSizeInBits = (drflac_uint8)(DRFLAC_CACHE_L1_BITS_REMAINING(&pFlac->bs) & 7); + if (paddingSizeInBits > 0) { + drflac_uint8 padding = 0; + if (!drflac__read_uint8(&pFlac->bs, paddingSizeInBits, &padding)) { + return DRFLAC_AT_END; } } - // Padding and CRC. - return drflac__seek_bits(pFlac, (DRFLAC_CACHE_L1_BITS_REMAINING & 7) + 16); +#ifndef DR_FLAC_NO_CRC + actualCRC16 = drflac__flush_crc16(&pFlac->bs); +#endif + if (!drflac__read_uint16(&pFlac->bs, 16, &desiredCRC16)) { + return DRFLAC_AT_END; + } + +#ifndef DR_FLAC_NO_CRC + if (actualCRC16 != desiredCRC16) { + return DRFLAC_CRC_MISMATCH; /* CRC mismatch. */ + } +#endif + + pFlac->currentFLACFrame.pcmFramesRemaining = pFlac->currentFLACFrame.header.blockSizeInPCMFrames; + + return DRFLAC_SUCCESS; } -static bool drflac__read_and_decode_next_frame(drflac* pFlac) +static drflac_result drflac__seek_flac_frame(drflac* pFlac) { - assert(pFlac != NULL); + int channelCount; + int i; + drflac_uint16 desiredCRC16; +#ifndef DR_FLAC_NO_CRC + drflac_uint16 actualCRC16; +#endif - if (!drflac__read_next_frame_header(pFlac)) { - return false; + channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + for (i = 0; i < channelCount; ++i) { + if (!drflac__seek_subframe(&pFlac->bs, &pFlac->currentFLACFrame, i)) { + return DRFLAC_ERROR; + } } - return drflac__decode_frame(pFlac); + /* Padding. */ + if (!drflac__seek_bits(&pFlac->bs, DRFLAC_CACHE_L1_BITS_REMAINING(&pFlac->bs) & 7)) { + return DRFLAC_ERROR; + } + + /* CRC. */ +#ifndef DR_FLAC_NO_CRC + actualCRC16 = drflac__flush_crc16(&pFlac->bs); +#endif + if (!drflac__read_uint16(&pFlac->bs, 16, &desiredCRC16)) { + return DRFLAC_AT_END; + } + +#ifndef DR_FLAC_NO_CRC + if (actualCRC16 != desiredCRC16) { + return DRFLAC_CRC_MISMATCH; /* CRC mismatch. */ + } +#endif + + return DRFLAC_SUCCESS; } -static unsigned int drflac__read_block_header(drflac* pFlac, unsigned int* pBlockSizeOut, bool* pIsLastBlockOut) // Returns the block type. +static drflac_bool32 drflac__read_and_decode_next_flac_frame(drflac* pFlac) { - assert(pFlac != NULL); + DRFLAC_ASSERT(pFlac != NULL); - unsigned char isLastBlock = 1; - unsigned char blockType = DRFLAC_BLOCK_TYPE_INVALID; - unsigned int blockSize = 0; + for (;;) { + drflac_result result; - if (!drflac__read_uint8(pFlac, 1, &isLastBlock)) { - goto done_reading_block_header; - } + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } - if (!drflac__read_uint8(pFlac, 7, &blockType)) { - goto done_reading_block_header; - } + result = drflac__decode_flac_frame(pFlac); + if (result != DRFLAC_SUCCESS) { + if (result == DRFLAC_CRC_MISMATCH) { + continue; /* CRC mismatch. Skip to the next frame. */ + } else { + return DRFLAC_FALSE; + } + } - if (!drflac__read_uint32(pFlac, 24, &blockSize)) { - goto done_reading_block_header; - } - - -done_reading_block_header: - if (pBlockSizeOut) { - *pBlockSizeOut = blockSize; - } - - if (pIsLastBlockOut) { - *pIsLastBlockOut = (isLastBlock != 0); - } - - return blockType; -} - - -static void drflac__get_current_frame_sample_range(drflac* pFlac, uint64_t* pFirstSampleInFrameOut, uint64_t* pLastSampleInFrameOut) -{ - assert(pFlac != NULL); - - unsigned int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFrame.channelAssignment); - - uint64_t firstSampleInFrame = pFlac->currentFrame.sampleNumber; - if (firstSampleInFrame == 0) { - firstSampleInFrame = pFlac->currentFrame.frameNumber * pFlac->maxBlockSize*channelCount; - } - - uint64_t lastSampleInFrame = firstSampleInFrame + (pFlac->currentFrame.blockSize*channelCount); - if (lastSampleInFrame > 0) { - lastSampleInFrame -= 1; // Needs to be zero based. - } - - - if (pFirstSampleInFrameOut) { - *pFirstSampleInFrameOut = firstSampleInFrame; - } - if (pLastSampleInFrameOut) { - *pLastSampleInFrameOut = lastSampleInFrame; + return DRFLAC_TRUE; } } -static bool drflac__seek_to_first_frame(drflac* pFlac) +static void drflac__get_pcm_frame_range_of_current_flac_frame(drflac* pFlac, drflac_uint64* pFirstPCMFrame, drflac_uint64* pLastPCMFrame) { - assert(pFlac != NULL); + drflac_uint64 firstPCMFrame; + drflac_uint64 lastPCMFrame; - bool result = drflac__seek_to_byte(pFlac, (long long)pFlac->firstFramePos); - pFlac->consumedBits = DRFLAC_CACHE_L1_SIZE_BITS; - pFlac->cache = 0; + DRFLAC_ASSERT(pFlac != NULL); - memset(&pFlac->currentFrame, 0, sizeof(pFlac->currentFrame)); + firstPCMFrame = pFlac->currentFLACFrame.header.pcmFrameNumber; + if (firstPCMFrame == 0) { + firstPCMFrame = ((drflac_uint64)pFlac->currentFLACFrame.header.flacFrameNumber) * pFlac->maxBlockSizeInPCMFrames; + } + lastPCMFrame = firstPCMFrame + pFlac->currentFLACFrame.header.blockSizeInPCMFrames; + if (lastPCMFrame > 0) { + lastPCMFrame -= 1; /* Needs to be zero based. */ + } + + if (pFirstPCMFrame) { + *pFirstPCMFrame = firstPCMFrame; + } + if (pLastPCMFrame) { + *pLastPCMFrame = lastPCMFrame; + } +} + +static drflac_bool32 drflac__seek_to_first_frame(drflac* pFlac) +{ + drflac_bool32 result; + + DRFLAC_ASSERT(pFlac != NULL); + + result = drflac__seek_to_byte(&pFlac->bs, pFlac->firstFLACFramePosInBytes); + + DRFLAC_ZERO_MEMORY(&pFlac->currentFLACFrame, sizeof(pFlac->currentFLACFrame)); + pFlac->currentPCMFrame = 0; return result; } -static DRFLAC_INLINE bool drflac__seek_to_next_frame(drflac* pFlac) +static DRFLAC_INLINE drflac_result drflac__seek_to_next_flac_frame(drflac* pFlac) { - // This function should only ever be called while the decoder is sitting on the first byte past the FRAME_HEADER section. - assert(pFlac != NULL); - return drflac__seek_frame(pFlac); -} - -static bool drflac__seek_to_frame_containing_sample(drflac* pFlac, uint64_t sampleIndex) -{ - assert(pFlac != NULL); - - if (!drflac__seek_to_first_frame(pFlac)) { - return false; - } - - uint64_t firstSampleInFrame = 0; - uint64_t lastSampleInFrame = 0; - for (;;) - { - // We need to read the frame's header in order to determine the range of samples it contains. - if (!drflac__read_next_frame_header(pFlac)) { - return false; - } - - drflac__get_current_frame_sample_range(pFlac, &firstSampleInFrame, &lastSampleInFrame); - if (sampleIndex >= firstSampleInFrame && sampleIndex <= lastSampleInFrame) { - break; // The sample is in this frame. - } - - if (!drflac__seek_to_next_frame(pFlac)) { - return false; - } - } - - // If we get here we should be right at the start of the frame containing the sample. - return true; -} - -static bool drflac__seek_to_sample__brute_force(drflac* pFlac, uint64_t sampleIndex) -{ - if (!drflac__seek_to_frame_containing_sample(pFlac, sampleIndex)) { - return false; - } - - // At this point we should be sitting on the first byte of the frame containing the sample. We need to decode every sample up to (but - // not including) the sample we're seeking to. - uint64_t firstSampleInFrame = 0; - drflac__get_current_frame_sample_range(pFlac, &firstSampleInFrame, NULL); - - assert(firstSampleInFrame <= sampleIndex); - size_t samplesToDecode = (size_t)(sampleIndex - firstSampleInFrame); // <-- Safe cast because the maximum number of samples in a frame is 65535. - if (samplesToDecode == 0) { - return true; - } - - // At this point we are just sitting on the byte after the frame header. We need to decode the frame before reading anything from it. - if (!drflac__decode_frame(pFlac)) { - return false; - } - - return (drflac_read_s16(pFlac, samplesToDecode, NULL) != 0); -} - -static bool drflac__seek_to_sample__seek_table(drflac* pFlac, uint64_t sampleIndex) -{ - assert(pFlac != NULL); - - if (pFlac->seektableBlock.pos == 0) { - return false; - } - - if (!drflac__seek_to_byte(pFlac, pFlac->seektableBlock.pos)) { - return false; - } - - // The number of seek points is derived from the size of the SEEKTABLE block. - unsigned int seekpointCount = pFlac->seektableBlock.sizeInBytes / 18; // 18 = the size of each seek point. - if (seekpointCount == 0) { - return false; // Would this ever happen? - } - - - drflac_seekpoint closestSeekpoint = {0}; - - unsigned int seekpointsRemaining = seekpointCount; - while (seekpointsRemaining > 0) - { - drflac_seekpoint seekpoint; - if (!drflac__read_uint64(pFlac, 64, &seekpoint.firstSample)) { - break; - } - if (!drflac__read_uint64(pFlac, 64, &seekpoint.frameOffset)) { - break; - } - if (!drflac__read_uint16(pFlac, 16, &seekpoint.sampleCount)) { - break; - } - - if (seekpoint.firstSample * pFlac->channels > sampleIndex) { - break; - } - - closestSeekpoint = seekpoint; - seekpointsRemaining -= 1; - } - - // At this point we should have found the seekpoint closest to our sample. We need to seek to it using basically the same - // technique as we use with the brute force method. - drflac__seek_to_byte(pFlac, pFlac->firstFramePos + closestSeekpoint.frameOffset); - - uint64_t firstSampleInFrame = 0; - uint64_t lastSampleInFrame = 0; - for (;;) - { - // We need to read the frame's header in order to determine the range of samples it contains. - if (!drflac__read_next_frame_header(pFlac)) { - return false; - } - - drflac__get_current_frame_sample_range(pFlac, &firstSampleInFrame, &lastSampleInFrame); - if (sampleIndex >= firstSampleInFrame && sampleIndex <= lastSampleInFrame) { - break; // The sample is in this frame. - } - - if (!drflac__seek_to_next_frame(pFlac)) { - return false; - } - } - - assert(firstSampleInFrame <= sampleIndex); - - // At this point we are just sitting on the byte after the frame header. We need to decode the frame before reading anything from it. - if (!drflac__decode_frame(pFlac)) { - return false; - } - - size_t samplesToDecode = (size_t)(sampleIndex - firstSampleInFrame); // <-- Safe cast because the maximum number of samples in a frame is 65535. - return drflac_read_s16(pFlac, samplesToDecode, NULL) == samplesToDecode; + /* This function should only ever be called while the decoder is sitting on the first byte past the FRAME_HEADER section. */ + DRFLAC_ASSERT(pFlac != NULL); + return drflac__seek_flac_frame(pFlac); } -static drflac* drflac_open(drflac_read_proc onRead, drflac_seek_proc onSeek, void* pUserData) +static drflac_uint64 drflac__seek_forward_by_pcm_frames(drflac* pFlac, drflac_uint64 pcmFramesToSeek) { - if (onRead == NULL || onSeek == NULL) { + drflac_uint64 pcmFramesRead = 0; + while (pcmFramesToSeek > 0) { + if (pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + break; /* Couldn't read the next frame, so just break from the loop and return. */ + } + } else { + if (pFlac->currentFLACFrame.pcmFramesRemaining > pcmFramesToSeek) { + pcmFramesRead += pcmFramesToSeek; + pFlac->currentFLACFrame.pcmFramesRemaining -= (drflac_uint32)pcmFramesToSeek; /* <-- Safe cast. Will always be < currentFrame.pcmFramesRemaining < 65536. */ + pcmFramesToSeek = 0; + } else { + pcmFramesRead += pFlac->currentFLACFrame.pcmFramesRemaining; + pcmFramesToSeek -= pFlac->currentFLACFrame.pcmFramesRemaining; + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + } + } + } + + pFlac->currentPCMFrame += pcmFramesRead; + return pcmFramesRead; +} + + +static drflac_bool32 drflac__seek_to_pcm_frame__brute_force(drflac* pFlac, drflac_uint64 pcmFrameIndex) +{ + drflac_bool32 isMidFrame = DRFLAC_FALSE; + drflac_uint64 runningPCMFrameCount; + + DRFLAC_ASSERT(pFlac != NULL); + + /* If we are seeking forward we start from the current position. Otherwise we need to start all the way from the start of the file. */ + if (pcmFrameIndex >= pFlac->currentPCMFrame) { + /* Seeking forward. Need to seek from the current position. */ + runningPCMFrameCount = pFlac->currentPCMFrame; + + /* The frame header for the first frame may not yet have been read. We need to do that if necessary. */ + if (pFlac->currentPCMFrame == 0 && pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } else { + isMidFrame = DRFLAC_TRUE; + } + } else { + /* Seeking backwards. Need to seek from the start of the file. */ + runningPCMFrameCount = 0; + + /* Move back to the start. */ + if (!drflac__seek_to_first_frame(pFlac)) { + return DRFLAC_FALSE; + } + + /* Decode the first frame in preparation for sample-exact seeking below. */ + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } + + /* + We need to as quickly as possible find the frame that contains the target sample. To do this, we iterate over each frame and inspect its + header. If based on the header we can determine that the frame contains the sample, we do a full decode of that frame. + */ + for (;;) { + drflac_uint64 pcmFrameCountInThisFLACFrame; + drflac_uint64 firstPCMFrameInFLACFrame = 0; + drflac_uint64 lastPCMFrameInFLACFrame = 0; + + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &firstPCMFrameInFLACFrame, &lastPCMFrameInFLACFrame); + + pcmFrameCountInThisFLACFrame = (lastPCMFrameInFLACFrame - firstPCMFrameInFLACFrame) + 1; + if (pcmFrameIndex < (runningPCMFrameCount + pcmFrameCountInThisFLACFrame)) { + /* + The sample should be in this frame. We need to fully decode it, however if it's an invalid frame (a CRC mismatch), we need to pretend + it never existed and keep iterating. + */ + drflac_uint64 pcmFramesToDecode = pcmFrameIndex - runningPCMFrameCount; + + if (!isMidFrame) { + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + /* The frame is valid. We just need to skip over some samples to ensure it's sample-exact. */ + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; /* <-- If this fails, something bad has happened (it should never fail). */ + } else { + if (result == DRFLAC_CRC_MISMATCH) { + goto next_iteration; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* We started seeking mid-frame which means we need to skip the frame decoding part. */ + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; + } + } else { + /* + It's not in this frame. We need to seek past the frame, but check if there was a CRC mismatch. If so, we pretend this + frame never existed and leave the running sample count untouched. + */ + if (!isMidFrame) { + drflac_result result = drflac__seek_to_next_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + runningPCMFrameCount += pcmFrameCountInThisFLACFrame; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + goto next_iteration; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* + We started seeking mid-frame which means we need to seek by reading to the end of the frame instead of with + drflac__seek_to_next_flac_frame() which only works if the decoder is sitting on the byte just after the frame header. + */ + runningPCMFrameCount += pFlac->currentFLACFrame.pcmFramesRemaining; + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + isMidFrame = DRFLAC_FALSE; + } + + /* If we are seeking to the end of the file and we've just hit it, we're done. */ + if (pcmFrameIndex == pFlac->totalPCMFrameCount && runningPCMFrameCount == pFlac->totalPCMFrameCount) { + return DRFLAC_TRUE; + } + } + + next_iteration: + /* Grab the next frame in preparation for the next iteration. */ + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } +} + + +#if !defined(DR_FLAC_NO_CRC) +/* +We use an average compression ratio to determine our approximate start location. FLAC files are generally about 50%-70% the size of their +uncompressed counterparts so we'll use this as a basis. I'm going to split the middle and use a factor of 0.6 to determine the starting +location. +*/ +#define DRFLAC_BINARY_SEARCH_APPROX_COMPRESSION_RATIO 0.6f + +static drflac_bool32 drflac__seek_to_approximate_flac_frame_to_byte(drflac* pFlac, drflac_uint64 targetByte, drflac_uint64 rangeLo, drflac_uint64 rangeHi, drflac_uint64* pLastSuccessfulSeekOffset) +{ + DRFLAC_ASSERT(pFlac != NULL); + DRFLAC_ASSERT(pLastSuccessfulSeekOffset != NULL); + DRFLAC_ASSERT(targetByte >= rangeLo); + DRFLAC_ASSERT(targetByte <= rangeHi); + + *pLastSuccessfulSeekOffset = pFlac->firstFLACFramePosInBytes; + + for (;;) { + /* After rangeLo == rangeHi == targetByte fails, we need to break out. */ + drflac_uint64 lastTargetByte = targetByte; + + /* When seeking to a byte, failure probably means we've attempted to seek beyond the end of the stream. To counter this we just halve it each attempt. */ + if (!drflac__seek_to_byte(&pFlac->bs, targetByte)) { + /* If we couldn't even seek to the first byte in the stream we have a problem. Just abandon the whole thing. */ + if (targetByte == 0) { + drflac__seek_to_first_frame(pFlac); /* Try to recover. */ + return DRFLAC_FALSE; + } + + /* Halve the byte location and continue. */ + targetByte = rangeLo + ((rangeHi - rangeLo)/2); + rangeHi = targetByte; + } else { + /* Getting here should mean that we have seeked to an appropriate byte. */ + + /* Clear the details of the FLAC frame so we don't misreport data. */ + DRFLAC_ZERO_MEMORY(&pFlac->currentFLACFrame, sizeof(pFlac->currentFLACFrame)); + + /* + Now seek to the next FLAC frame. We need to decode the entire frame (not just the header) because it's possible for the header to incorrectly pass the + CRC check and return bad data. We need to decode the entire frame to be more certain. Although this seems unlikely, this has happened to me in testing + so it needs to stay this way for now. + */ +#if 1 + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + /* Halve the byte location and continue. */ + targetByte = rangeLo + ((rangeHi - rangeLo)/2); + rangeHi = targetByte; + } else { + break; + } +#else + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + /* Halve the byte location and continue. */ + targetByte = rangeLo + ((rangeHi - rangeLo)/2); + rangeHi = targetByte; + } else { + break; + } +#endif + } + + /* We already tried this byte and there are no more to try, break out. */ + if(targetByte == lastTargetByte) { + return DRFLAC_FALSE; + } + } + + /* The current PCM frame needs to be updated based on the frame we just seeked to. */ + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &pFlac->currentPCMFrame, NULL); + + DRFLAC_ASSERT(targetByte <= rangeHi); + + *pLastSuccessfulSeekOffset = targetByte; + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__decode_flac_frame_and_seek_forward_by_pcm_frames(drflac* pFlac, drflac_uint64 offset) +{ + /* This section of code would be used if we were only decoding the FLAC frame header when calling drflac__seek_to_approximate_flac_frame_to_byte(). */ +#if 0 + if (drflac__decode_flac_frame(pFlac) != DRFLAC_SUCCESS) { + /* We failed to decode this frame which may be due to it being corrupt. We'll just use the next valid FLAC frame. */ + if (drflac__read_and_decode_next_flac_frame(pFlac) == DRFLAC_FALSE) { + return DRFLAC_FALSE; + } + } +#endif + + return drflac__seek_forward_by_pcm_frames(pFlac, offset) == offset; +} + + +static drflac_bool32 drflac__seek_to_pcm_frame__binary_search_internal(drflac* pFlac, drflac_uint64 pcmFrameIndex, drflac_uint64 byteRangeLo, drflac_uint64 byteRangeHi) +{ + /* This assumes pFlac->currentPCMFrame is sitting on byteRangeLo upon entry. */ + + drflac_uint64 targetByte; + drflac_uint64 pcmRangeLo = pFlac->totalPCMFrameCount; + drflac_uint64 pcmRangeHi = 0; + drflac_uint64 lastSuccessfulSeekOffset = (drflac_uint64)-1; + drflac_uint64 closestSeekOffsetBeforeTargetPCMFrame = byteRangeLo; + drflac_uint32 seekForwardThreshold = (pFlac->maxBlockSizeInPCMFrames != 0) ? pFlac->maxBlockSizeInPCMFrames*2 : 4096; + + targetByte = byteRangeLo + (drflac_uint64)(((drflac_int64)((pcmFrameIndex - pFlac->currentPCMFrame) * pFlac->channels * pFlac->bitsPerSample)/8.0f) * DRFLAC_BINARY_SEARCH_APPROX_COMPRESSION_RATIO); + if (targetByte > byteRangeHi) { + targetByte = byteRangeHi; + } + + for (;;) { + if (drflac__seek_to_approximate_flac_frame_to_byte(pFlac, targetByte, byteRangeLo, byteRangeHi, &lastSuccessfulSeekOffset)) { + /* We found a FLAC frame. We need to check if it contains the sample we're looking for. */ + drflac_uint64 newPCMRangeLo; + drflac_uint64 newPCMRangeHi; + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &newPCMRangeLo, &newPCMRangeHi); + + /* If we selected the same frame, it means we should be pretty close. Just decode the rest. */ + if (pcmRangeLo == newPCMRangeLo) { + if (!drflac__seek_to_approximate_flac_frame_to_byte(pFlac, closestSeekOffsetBeforeTargetPCMFrame, closestSeekOffsetBeforeTargetPCMFrame, byteRangeHi, &lastSuccessfulSeekOffset)) { + break; /* Failed to seek to closest frame. */ + } + + if (drflac__decode_flac_frame_and_seek_forward_by_pcm_frames(pFlac, pcmFrameIndex - pFlac->currentPCMFrame)) { + return DRFLAC_TRUE; + } else { + break; /* Failed to seek forward. */ + } + } + + pcmRangeLo = newPCMRangeLo; + pcmRangeHi = newPCMRangeHi; + + if (pcmRangeLo <= pcmFrameIndex && pcmRangeHi >= pcmFrameIndex) { + /* The target PCM frame is in this FLAC frame. */ + if (drflac__decode_flac_frame_and_seek_forward_by_pcm_frames(pFlac, pcmFrameIndex - pFlac->currentPCMFrame) ) { + return DRFLAC_TRUE; + } else { + break; /* Failed to seek to FLAC frame. */ + } + } else { + const float approxCompressionRatio = (drflac_int64)(lastSuccessfulSeekOffset - pFlac->firstFLACFramePosInBytes) / ((drflac_int64)(pcmRangeLo * pFlac->channels * pFlac->bitsPerSample)/8.0f); + + if (pcmRangeLo > pcmFrameIndex) { + /* We seeked too far forward. We need to move our target byte backward and try again. */ + byteRangeHi = lastSuccessfulSeekOffset; + if (byteRangeLo > byteRangeHi) { + byteRangeLo = byteRangeHi; + } + + targetByte = byteRangeLo + ((byteRangeHi - byteRangeLo) / 2); + if (targetByte < byteRangeLo) { + targetByte = byteRangeLo; + } + } else /*if (pcmRangeHi < pcmFrameIndex)*/ { + /* We didn't seek far enough. We need to move our target byte forward and try again. */ + + /* If we're close enough we can just seek forward. */ + if ((pcmFrameIndex - pcmRangeLo) < seekForwardThreshold) { + if (drflac__decode_flac_frame_and_seek_forward_by_pcm_frames(pFlac, pcmFrameIndex - pFlac->currentPCMFrame)) { + return DRFLAC_TRUE; + } else { + break; /* Failed to seek to FLAC frame. */ + } + } else { + byteRangeLo = lastSuccessfulSeekOffset; + if (byteRangeHi < byteRangeLo) { + byteRangeHi = byteRangeLo; + } + + targetByte = lastSuccessfulSeekOffset + (drflac_uint64)(((drflac_int64)((pcmFrameIndex-pcmRangeLo) * pFlac->channels * pFlac->bitsPerSample)/8.0f) * approxCompressionRatio); + if (targetByte > byteRangeHi) { + targetByte = byteRangeHi; + } + + if (closestSeekOffsetBeforeTargetPCMFrame < lastSuccessfulSeekOffset) { + closestSeekOffsetBeforeTargetPCMFrame = lastSuccessfulSeekOffset; + } + } + } + } + } else { + /* Getting here is really bad. We just recover as best we can, but moving to the first frame in the stream, and then abort. */ + break; + } + } + + drflac__seek_to_first_frame(pFlac); /* <-- Try to recover. */ + return DRFLAC_FALSE; +} + +static drflac_bool32 drflac__seek_to_pcm_frame__binary_search(drflac* pFlac, drflac_uint64 pcmFrameIndex) +{ + drflac_uint64 byteRangeLo; + drflac_uint64 byteRangeHi; + drflac_uint32 seekForwardThreshold = (pFlac->maxBlockSizeInPCMFrames != 0) ? pFlac->maxBlockSizeInPCMFrames*2 : 4096; + + /* Our algorithm currently assumes the FLAC stream is currently sitting at the start. */ + if (drflac__seek_to_first_frame(pFlac) == DRFLAC_FALSE) { + return DRFLAC_FALSE; + } + + /* If we're close enough to the start, just move to the start and seek forward. */ + if (pcmFrameIndex < seekForwardThreshold) { + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFrameIndex) == pcmFrameIndex; + } + + /* + Our starting byte range is the byte position of the first FLAC frame and the approximate end of the file as if it were completely uncompressed. This ensures + the entire file is included, even though most of the time it'll exceed the end of the actual stream. This is OK as the frame searching logic will handle it. + */ + byteRangeLo = pFlac->firstFLACFramePosInBytes; + byteRangeHi = pFlac->firstFLACFramePosInBytes + (drflac_uint64)((drflac_int64)(pFlac->totalPCMFrameCount * pFlac->channels * pFlac->bitsPerSample)/8.0f); + + return drflac__seek_to_pcm_frame__binary_search_internal(pFlac, pcmFrameIndex, byteRangeLo, byteRangeHi); +} +#endif /* !DR_FLAC_NO_CRC */ + +static drflac_bool32 drflac__seek_to_pcm_frame__seek_table(drflac* pFlac, drflac_uint64 pcmFrameIndex) +{ + drflac_uint32 iClosestSeekpoint = 0; + drflac_bool32 isMidFrame = DRFLAC_FALSE; + drflac_uint64 runningPCMFrameCount; + drflac_uint32 iSeekpoint; + + + DRFLAC_ASSERT(pFlac != NULL); + + if (pFlac->pSeekpoints == NULL || pFlac->seekpointCount == 0) { + return DRFLAC_FALSE; + } + + /* Do not use the seektable if pcmFramIndex is not coverd by it. */ + if (pFlac->pSeekpoints[0].firstPCMFrame > pcmFrameIndex) { + return DRFLAC_FALSE; + } + + for (iSeekpoint = 0; iSeekpoint < pFlac->seekpointCount; ++iSeekpoint) { + if (pFlac->pSeekpoints[iSeekpoint].firstPCMFrame >= pcmFrameIndex) { + break; + } + + iClosestSeekpoint = iSeekpoint; + } + + /* There's been cases where the seek table contains only zeros. We need to do some basic validation on the closest seekpoint. */ + if (pFlac->pSeekpoints[iClosestSeekpoint].pcmFrameCount == 0 || pFlac->pSeekpoints[iClosestSeekpoint].pcmFrameCount > pFlac->maxBlockSizeInPCMFrames) { + return DRFLAC_FALSE; + } + if (pFlac->pSeekpoints[iClosestSeekpoint].firstPCMFrame > pFlac->totalPCMFrameCount && pFlac->totalPCMFrameCount > 0) { + return DRFLAC_FALSE; + } + +#if !defined(DR_FLAC_NO_CRC) + /* At this point we should know the closest seek point. We can use a binary search for this. We need to know the total sample count for this. */ + if (pFlac->totalPCMFrameCount > 0) { + drflac_uint64 byteRangeLo; + drflac_uint64 byteRangeHi; + + byteRangeHi = pFlac->firstFLACFramePosInBytes + (drflac_uint64)((drflac_int64)(pFlac->totalPCMFrameCount * pFlac->channels * pFlac->bitsPerSample)/8.0f); + byteRangeLo = pFlac->firstFLACFramePosInBytes + pFlac->pSeekpoints[iClosestSeekpoint].flacFrameOffset; + + /* + If our closest seek point is not the last one, we only need to search between it and the next one. The section below calculates an appropriate starting + value for byteRangeHi which will clamp it appropriately. + + Note that the next seekpoint must have an offset greater than the closest seekpoint because otherwise our binary search algorithm will break down. There + have been cases where a seektable consists of seek points where every byte offset is set to 0 which causes problems. If this happens we need to abort. + */ + if (iClosestSeekpoint < pFlac->seekpointCount-1) { + drflac_uint32 iNextSeekpoint = iClosestSeekpoint + 1; + + /* Basic validation on the seekpoints to ensure they're usable. */ + if (pFlac->pSeekpoints[iClosestSeekpoint].flacFrameOffset >= pFlac->pSeekpoints[iNextSeekpoint].flacFrameOffset || pFlac->pSeekpoints[iNextSeekpoint].pcmFrameCount == 0) { + return DRFLAC_FALSE; /* The next seekpoint doesn't look right. The seek table cannot be trusted from here. Abort. */ + } + + if (pFlac->pSeekpoints[iNextSeekpoint].firstPCMFrame != (((drflac_uint64)0xFFFFFFFF << 32) | 0xFFFFFFFF)) { /* Make sure it's not a placeholder seekpoint. */ + byteRangeHi = pFlac->firstFLACFramePosInBytes + pFlac->pSeekpoints[iNextSeekpoint].flacFrameOffset - 1; /* byteRangeHi must be zero based. */ + } + } + + if (drflac__seek_to_byte(&pFlac->bs, pFlac->firstFLACFramePosInBytes + pFlac->pSeekpoints[iClosestSeekpoint].flacFrameOffset)) { + if (drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &pFlac->currentPCMFrame, NULL); + + if (drflac__seek_to_pcm_frame__binary_search_internal(pFlac, pcmFrameIndex, byteRangeLo, byteRangeHi)) { + return DRFLAC_TRUE; + } + } + } + } +#endif /* !DR_FLAC_NO_CRC */ + + /* Getting here means we need to use a slower algorithm because the binary search method failed or cannot be used. */ + + /* + If we are seeking forward and the closest seekpoint is _before_ the current sample, we just seek forward from where we are. Otherwise we start seeking + from the seekpoint's first sample. + */ + if (pcmFrameIndex >= pFlac->currentPCMFrame && pFlac->pSeekpoints[iClosestSeekpoint].firstPCMFrame <= pFlac->currentPCMFrame) { + /* Optimized case. Just seek forward from where we are. */ + runningPCMFrameCount = pFlac->currentPCMFrame; + + /* The frame header for the first frame may not yet have been read. We need to do that if necessary. */ + if (pFlac->currentPCMFrame == 0 && pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } else { + isMidFrame = DRFLAC_TRUE; + } + } else { + /* Slower case. Seek to the start of the seekpoint and then seek forward from there. */ + runningPCMFrameCount = pFlac->pSeekpoints[iClosestSeekpoint].firstPCMFrame; + + if (!drflac__seek_to_byte(&pFlac->bs, pFlac->firstFLACFramePosInBytes + pFlac->pSeekpoints[iClosestSeekpoint].flacFrameOffset)) { + return DRFLAC_FALSE; + } + + /* Grab the frame the seekpoint is sitting on in preparation for the sample-exact seeking below. */ + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } + + for (;;) { + drflac_uint64 pcmFrameCountInThisFLACFrame; + drflac_uint64 firstPCMFrameInFLACFrame = 0; + drflac_uint64 lastPCMFrameInFLACFrame = 0; + + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &firstPCMFrameInFLACFrame, &lastPCMFrameInFLACFrame); + + pcmFrameCountInThisFLACFrame = (lastPCMFrameInFLACFrame - firstPCMFrameInFLACFrame) + 1; + if (pcmFrameIndex < (runningPCMFrameCount + pcmFrameCountInThisFLACFrame)) { + /* + The sample should be in this frame. We need to fully decode it, but if it's an invalid frame (a CRC mismatch) we need to pretend + it never existed and keep iterating. + */ + drflac_uint64 pcmFramesToDecode = pcmFrameIndex - runningPCMFrameCount; + + if (!isMidFrame) { + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + /* The frame is valid. We just need to skip over some samples to ensure it's sample-exact. */ + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; /* <-- If this fails, something bad has happened (it should never fail). */ + } else { + if (result == DRFLAC_CRC_MISMATCH) { + goto next_iteration; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* We started seeking mid-frame which means we need to skip the frame decoding part. */ + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; + } + } else { + /* + It's not in this frame. We need to seek past the frame, but check if there was a CRC mismatch. If so, we pretend this + frame never existed and leave the running sample count untouched. + */ + if (!isMidFrame) { + drflac_result result = drflac__seek_to_next_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + runningPCMFrameCount += pcmFrameCountInThisFLACFrame; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + goto next_iteration; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* + We started seeking mid-frame which means we need to seek by reading to the end of the frame instead of with + drflac__seek_to_next_flac_frame() which only works if the decoder is sitting on the byte just after the frame header. + */ + runningPCMFrameCount += pFlac->currentFLACFrame.pcmFramesRemaining; + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + isMidFrame = DRFLAC_FALSE; + } + + /* If we are seeking to the end of the file and we've just hit it, we're done. */ + if (pcmFrameIndex == pFlac->totalPCMFrameCount && runningPCMFrameCount == pFlac->totalPCMFrameCount) { + return DRFLAC_TRUE; + } + } + + next_iteration: + /* Grab the next frame in preparation for the next iteration. */ + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + } +} + + +#ifndef DR_FLAC_NO_OGG +typedef struct +{ + drflac_uint8 capturePattern[4]; /* Should be "OggS" */ + drflac_uint8 structureVersion; /* Always 0. */ + drflac_uint8 headerType; + drflac_uint64 granulePosition; + drflac_uint32 serialNumber; + drflac_uint32 sequenceNumber; + drflac_uint32 checksum; + drflac_uint8 segmentCount; + drflac_uint8 segmentTable[255]; +} drflac_ogg_page_header; +#endif + +typedef struct +{ + drflac_read_proc onRead; + drflac_seek_proc onSeek; + drflac_tell_proc onTell; + drflac_meta_proc onMeta; + drflac_container container; + void* pUserData; + void* pUserDataMD; + drflac_uint32 sampleRate; + drflac_uint8 channels; + drflac_uint8 bitsPerSample; + drflac_uint64 totalPCMFrameCount; + drflac_uint16 maxBlockSizeInPCMFrames; + drflac_uint64 runningFilePos; + drflac_bool32 hasStreamInfoBlock; + drflac_bool32 hasMetadataBlocks; + drflac_bs bs; /* <-- A bit streamer is required for loading data during initialization. */ + drflac_frame_header firstFrameHeader; /* <-- The header of the first frame that was read during relaxed initalization. Only set if there is no STREAMINFO block. */ + +#ifndef DR_FLAC_NO_OGG + drflac_uint32 oggSerial; + drflac_uint64 oggFirstBytePos; + drflac_ogg_page_header oggBosHeader; +#endif +} drflac_init_info; + +static DRFLAC_INLINE void drflac__decode_block_header(drflac_uint32 blockHeader, drflac_uint8* isLastBlock, drflac_uint8* blockType, drflac_uint32* blockSize) +{ + blockHeader = drflac__be2host_32(blockHeader); + *isLastBlock = (drflac_uint8)((blockHeader & 0x80000000UL) >> 31); + *blockType = (drflac_uint8)((blockHeader & 0x7F000000UL) >> 24); + *blockSize = (blockHeader & 0x00FFFFFFUL); +} + +static DRFLAC_INLINE drflac_bool32 drflac__read_and_decode_block_header(drflac_read_proc onRead, void* pUserData, drflac_uint8* isLastBlock, drflac_uint8* blockType, drflac_uint32* blockSize) +{ + drflac_uint32 blockHeader; + + *blockSize = 0; + if (onRead(pUserData, &blockHeader, 4) != 4) { + return DRFLAC_FALSE; + } + + drflac__decode_block_header(blockHeader, isLastBlock, blockType, blockSize); + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__read_streaminfo(drflac_read_proc onRead, void* pUserData, drflac_streaminfo* pStreamInfo) +{ + drflac_uint32 blockSizes; + drflac_uint64 frameSizes = 0; + drflac_uint64 importantProps; + drflac_uint8 md5[16]; + + /* min/max block size. */ + if (onRead(pUserData, &blockSizes, 4) != 4) { + return DRFLAC_FALSE; + } + + /* min/max frame size. */ + if (onRead(pUserData, &frameSizes, 6) != 6) { + return DRFLAC_FALSE; + } + + /* Sample rate, channels, bits per sample and total sample count. */ + if (onRead(pUserData, &importantProps, 8) != 8) { + return DRFLAC_FALSE; + } + + /* MD5 */ + if (onRead(pUserData, md5, sizeof(md5)) != sizeof(md5)) { + return DRFLAC_FALSE; + } + + blockSizes = drflac__be2host_32(blockSizes); + frameSizes = drflac__be2host_64(frameSizes); + importantProps = drflac__be2host_64(importantProps); + + pStreamInfo->minBlockSizeInPCMFrames = (drflac_uint16)((blockSizes & 0xFFFF0000) >> 16); + pStreamInfo->maxBlockSizeInPCMFrames = (drflac_uint16) (blockSizes & 0x0000FFFF); + pStreamInfo->minFrameSizeInPCMFrames = (drflac_uint32)((frameSizes & (((drflac_uint64)0x00FFFFFF << 16) << 24)) >> 40); + pStreamInfo->maxFrameSizeInPCMFrames = (drflac_uint32)((frameSizes & (((drflac_uint64)0x00FFFFFF << 16) << 0)) >> 16); + pStreamInfo->sampleRate = (drflac_uint32)((importantProps & (((drflac_uint64)0x000FFFFF << 16) << 28)) >> 44); + pStreamInfo->channels = (drflac_uint8 )((importantProps & (((drflac_uint64)0x0000000E << 16) << 24)) >> 41) + 1; + pStreamInfo->bitsPerSample = (drflac_uint8 )((importantProps & (((drflac_uint64)0x0000001F << 16) << 20)) >> 36) + 1; + pStreamInfo->totalPCMFrameCount = ((importantProps & ((((drflac_uint64)0x0000000F << 16) << 16) | 0xFFFFFFFF))); + DRFLAC_COPY_MEMORY(pStreamInfo->md5, md5, sizeof(md5)); + + return DRFLAC_TRUE; +} + + +static void* drflac__malloc_default(size_t sz, void* pUserData) +{ + (void)pUserData; + return DRFLAC_MALLOC(sz); +} + +static void* drflac__realloc_default(void* p, size_t sz, void* pUserData) +{ + (void)pUserData; + return DRFLAC_REALLOC(p, sz); +} + +static void drflac__free_default(void* p, void* pUserData) +{ + (void)pUserData; + DRFLAC_FREE(p); +} + + +static void* drflac__malloc_from_callbacks(size_t sz, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { return NULL; } - unsigned char id[4]; - if (onRead(pUserData, id, 4) != 4 || id[0] != 'f' || id[1] != 'L' || id[2] != 'a' || id[3] != 'C') { - return NULL; // Not a FLAC stream. + if (pAllocationCallbacks->onMalloc != NULL) { + return pAllocationCallbacks->onMalloc(sz, pAllocationCallbacks->pUserData); } - drflac tempFlac; - memset(&tempFlac, 0, sizeof(tempFlac)); - tempFlac.onRead = onRead; - tempFlac.onSeek = onSeek; - tempFlac.pUserData = pUserData; - tempFlac.currentBytePos = 4; - tempFlac.nextL2Line = sizeof(tempFlac.cacheL2) / sizeof(tempFlac.cacheL2[0]); // <-- Initialize to this to force a client-side data retrieval right from the start. - tempFlac.consumedBits = sizeof(tempFlac.cache)*8; + /* Try using realloc(). */ + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(NULL, sz, pAllocationCallbacks->pUserData); + } - // The first metadata block should be the STREAMINFO block. We don't care about everything in here. - unsigned int blockSize; - bool isLastBlock; - int blockType = drflac__read_block_header(&tempFlac, &blockSize, &isLastBlock); - if (blockType != DRFLAC_BLOCK_TYPE_STREAMINFO && blockSize != 34) { + return NULL; +} + +static void* drflac__realloc_from_callbacks(void* p, size_t szNew, size_t szOld, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { return NULL; } - if (!drflac__seek_bits(&tempFlac, 16)) { // minBlockSize - return NULL; - } - if (!drflac__read_uint16(&tempFlac, 16, &tempFlac.maxBlockSize)) { - return NULL; - } - if (!drflac__seek_bits(&tempFlac, 48)) { // minFrameSize + maxFrameSize - return NULL; - } - if (!drflac__read_uint32(&tempFlac, 20, &tempFlac.sampleRate)) { - return NULL; - } - if (!drflac__read_uint8(&tempFlac, 3, &tempFlac.channels)) { - return NULL; - } - if (!drflac__read_uint8(&tempFlac, 5, &tempFlac.bitsPerSample)) { - return NULL; - } - if (!drflac__read_uint64(&tempFlac, 36, &tempFlac.totalSampleCount)) { - return NULL; - } - if (!drflac__seek_bits(&tempFlac, 128)) { // MD5 - return NULL; + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(p, szNew, pAllocationCallbacks->pUserData); } - tempFlac.channels += 1; - tempFlac.bitsPerSample += 1; - tempFlac.totalSampleCount *= tempFlac.channels; + /* Try emulating realloc() in terms of malloc()/free(). */ + if (pAllocationCallbacks->onMalloc != NULL && pAllocationCallbacks->onFree != NULL) { + void* p2; - while (!isLastBlock) - { - blockType = drflac__read_block_header(&tempFlac, &blockSize, &isLastBlock); + p2 = pAllocationCallbacks->onMalloc(szNew, pAllocationCallbacks->pUserData); + if (p2 == NULL) { + return NULL; + } + + if (p != NULL) { + DRFLAC_COPY_MEMORY(p2, p, szOld); + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } + + return p2; + } + + return NULL; +} + +static void drflac__free_from_callbacks(void* p, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (p == NULL || pAllocationCallbacks == NULL) { + return; + } + + if (pAllocationCallbacks->onFree != NULL) { + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } +} + + +static drflac_bool32 drflac__read_and_decode_metadata(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, void* pUserData, void* pUserDataMD, drflac_uint64* pFirstFramePos, drflac_uint64* pSeektablePos, drflac_uint32* pSeekpointCount, drflac_allocation_callbacks* pAllocationCallbacks) +{ + /* + We want to keep track of the byte position in the stream of the seektable. At the time of calling this function we know that + we'll be sitting on byte 42. + */ + drflac_uint64 runningFilePos = 42; + drflac_uint64 seektablePos = 0; + drflac_uint32 seektableSize = 0; + + (void)onTell; + + for (;;) { + drflac_metadata metadata; + drflac_uint8 isLastBlock = 0; + drflac_uint8 blockType = 0; + drflac_uint32 blockSize; + if (drflac__read_and_decode_block_header(onRead, pUserData, &isLastBlock, &blockType, &blockSize) == DRFLAC_FALSE) { + return DRFLAC_FALSE; + } + runningFilePos += 4; + + metadata.type = blockType; + metadata.rawDataSize = 0; + metadata.rawDataOffset = runningFilePos; + metadata.pRawData = NULL; switch (blockType) { - case DRFLAC_BLOCK_TYPE_APPLICATION: + case DRFLAC_METADATA_BLOCK_TYPE_APPLICATION: { - tempFlac.applicationBlock.pos = drflac__tell(&tempFlac); - tempFlac.applicationBlock.sizeInBytes = blockSize; + if (blockSize < 4) { + return DRFLAC_FALSE; + } + + if (onMeta) { + void* pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); + if (pRawData == NULL) { + return DRFLAC_FALSE; + } + + if (onRead(pUserData, pRawData, blockSize) != blockSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + metadata.data.application.id = drflac__be2host_32(*(drflac_uint32*)pRawData); + metadata.data.application.pData = (const void*)((drflac_uint8*)pRawData + sizeof(drflac_uint32)); + metadata.data.application.dataSize = blockSize - sizeof(drflac_uint32); + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + } } break; - case DRFLAC_BLOCK_TYPE_SEEKTABLE: + case DRFLAC_METADATA_BLOCK_TYPE_SEEKTABLE: { - tempFlac.seektableBlock.pos = drflac__tell(&tempFlac); - tempFlac.seektableBlock.sizeInBytes = blockSize; + seektablePos = runningFilePos; + seektableSize = blockSize; + + if (onMeta) { + drflac_uint32 seekpointCount; + drflac_uint32 iSeekpoint; + void* pRawData; + + seekpointCount = blockSize/DRFLAC_SEEKPOINT_SIZE_IN_BYTES; + + pRawData = drflac__malloc_from_callbacks(seekpointCount * sizeof(drflac_seekpoint), pAllocationCallbacks); + if (pRawData == NULL) { + return DRFLAC_FALSE; + } + + /* We need to read seekpoint by seekpoint and do some processing. */ + for (iSeekpoint = 0; iSeekpoint < seekpointCount; ++iSeekpoint) { + drflac_seekpoint* pSeekpoint = (drflac_seekpoint*)pRawData + iSeekpoint; + + if (onRead(pUserData, pSeekpoint, DRFLAC_SEEKPOINT_SIZE_IN_BYTES) != DRFLAC_SEEKPOINT_SIZE_IN_BYTES) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + /* Endian swap. */ + pSeekpoint->firstPCMFrame = drflac__be2host_64(pSeekpoint->firstPCMFrame); + pSeekpoint->flacFrameOffset = drflac__be2host_64(pSeekpoint->flacFrameOffset); + pSeekpoint->pcmFrameCount = drflac__be2host_16(pSeekpoint->pcmFrameCount); + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + metadata.data.seektable.seekpointCount = seekpointCount; + metadata.data.seektable.pSeekpoints = (const drflac_seekpoint*)pRawData; + + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + } } break; - case DRFLAC_BLOCK_TYPE_VORBIS_COMMENT: + case DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT: { - tempFlac.vorbisCommentBlock.pos = drflac__tell(&tempFlac); - tempFlac.vorbisCommentBlock.sizeInBytes = blockSize; + if (blockSize < 8) { + return DRFLAC_FALSE; + } + + if (onMeta) { + void* pRawData; + const char* pRunningData; + const char* pRunningDataEnd; + drflac_uint32 i; + + pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); + if (pRawData == NULL) { + return DRFLAC_FALSE; + } + + if (onRead(pUserData, pRawData, blockSize) != blockSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + + pRunningData = (const char*)pRawData; + pRunningDataEnd = (const char*)pRawData + blockSize; + + metadata.data.vorbis_comment.vendorLength = drflac__le2host_32_ptr_unaligned(pRunningData); pRunningData += 4; + + /* Need space for the rest of the block */ + if ((pRunningDataEnd - pRunningData) - 4 < (drflac_int64)metadata.data.vorbis_comment.vendorLength) { /* <-- Note the order of operations to avoid overflow to a valid value */ + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + metadata.data.vorbis_comment.vendor = pRunningData; pRunningData += metadata.data.vorbis_comment.vendorLength; + metadata.data.vorbis_comment.commentCount = drflac__le2host_32_ptr_unaligned(pRunningData); pRunningData += 4; + + /* Need space for 'commentCount' comments after the block, which at minimum is a drflac_uint32 per comment */ + if ((pRunningDataEnd - pRunningData) / sizeof(drflac_uint32) < metadata.data.vorbis_comment.commentCount) { /* <-- Note the order of operations to avoid overflow to a valid value */ + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + metadata.data.vorbis_comment.pComments = pRunningData; + + /* Check that the comments section is valid before passing it to the callback */ + for (i = 0; i < metadata.data.vorbis_comment.commentCount; ++i) { + drflac_uint32 commentLength; + + if (pRunningDataEnd - pRunningData < 4) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + commentLength = drflac__le2host_32_ptr_unaligned(pRunningData); pRunningData += 4; + if (pRunningDataEnd - pRunningData < (drflac_int64)commentLength) { /* <-- Note the order of operations to avoid overflow to a valid value */ + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + pRunningData += commentLength; + } + + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + } } break; - case DRFLAC_BLOCK_TYPE_CUESHEET: + case DRFLAC_METADATA_BLOCK_TYPE_CUESHEET: { - tempFlac.cuesheetBlock.pos = drflac__tell(&tempFlac); - tempFlac.cuesheetBlock.sizeInBytes = blockSize; + if (blockSize < 396) { + return DRFLAC_FALSE; + } + + if (onMeta) { + void* pRawData; + const char* pRunningData; + const char* pRunningDataEnd; + size_t bufferSize; + drflac_uint8 iTrack; + drflac_uint8 iIndex; + void* pTrackData; + + /* + This needs to be loaded in two passes. The first pass is used to calculate the size of the memory allocation + we need for storing the necessary data. The second pass will fill that buffer with usable data. + */ + pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); + if (pRawData == NULL) { + return DRFLAC_FALSE; + } + + if (onRead(pUserData, pRawData, blockSize) != blockSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + + pRunningData = (const char*)pRawData; + pRunningDataEnd = (const char*)pRawData + blockSize; + + DRFLAC_COPY_MEMORY(metadata.data.cuesheet.catalog, pRunningData, 128); pRunningData += 128; + metadata.data.cuesheet.leadInSampleCount = drflac__be2host_64(*(const drflac_uint64*)pRunningData); pRunningData += 8; + metadata.data.cuesheet.isCD = (pRunningData[0] & 0x80) != 0; pRunningData += 259; + metadata.data.cuesheet.trackCount = pRunningData[0]; pRunningData += 1; + metadata.data.cuesheet.pTrackData = NULL; /* Will be filled later. */ + + /* Pass 1: Calculate the size of the buffer for the track data. */ + { + const char* pRunningDataSaved = pRunningData; /* Will be restored at the end in preparation for the second pass. */ + + bufferSize = metadata.data.cuesheet.trackCount * DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES; + + for (iTrack = 0; iTrack < metadata.data.cuesheet.trackCount; ++iTrack) { + drflac_uint8 indexCount; + drflac_uint32 indexPointSize; + + if (pRunningDataEnd - pRunningData < DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + /* Skip to the index point count */ + pRunningData += 35; + + indexCount = pRunningData[0]; + pRunningData += 1; + + bufferSize += indexCount * sizeof(drflac_cuesheet_track_index); + + /* Quick validation check. */ + indexPointSize = indexCount * DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES; + if (pRunningDataEnd - pRunningData < (drflac_int64)indexPointSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + pRunningData += indexPointSize; + } + + pRunningData = pRunningDataSaved; + } + + /* Pass 2: Allocate a buffer and fill the data. Validation was done in the step above so can be skipped. */ + { + char* pRunningTrackData; + + pTrackData = drflac__malloc_from_callbacks(bufferSize, pAllocationCallbacks); + if (pTrackData == NULL) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + + pRunningTrackData = (char*)pTrackData; + + for (iTrack = 0; iTrack < metadata.data.cuesheet.trackCount; ++iTrack) { + drflac_uint8 indexCount; + + DRFLAC_COPY_MEMORY(pRunningTrackData, pRunningData, DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES); + pRunningData += DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES-1; /* Skip forward, but not beyond the last byte in the CUESHEET_TRACK block which is the index count. */ + pRunningTrackData += DRFLAC_CUESHEET_TRACK_SIZE_IN_BYTES-1; + + /* Grab the index count for the next part. */ + indexCount = pRunningData[0]; + pRunningData += 1; + pRunningTrackData += 1; + + /* Extract each track index. */ + for (iIndex = 0; iIndex < indexCount; ++iIndex) { + drflac_cuesheet_track_index* pTrackIndex = (drflac_cuesheet_track_index*)pRunningTrackData; + + DRFLAC_COPY_MEMORY(pRunningTrackData, pRunningData, DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES); + pRunningData += DRFLAC_CUESHEET_TRACK_INDEX_SIZE_IN_BYTES; + pRunningTrackData += sizeof(drflac_cuesheet_track_index); + + pTrackIndex->offset = drflac__be2host_64(pTrackIndex->offset); + } + } + + metadata.data.cuesheet.pTrackData = pTrackData; + } + + /* The original data is no longer needed. */ + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + pRawData = NULL; + + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pTrackData, pAllocationCallbacks); + pTrackData = NULL; + } } break; - case DRFLAC_BLOCK_TYPE_PICTURE: + case DRFLAC_METADATA_BLOCK_TYPE_PICTURE: { - tempFlac.pictureBlock.pos = drflac__tell(&tempFlac); - tempFlac.pictureBlock.sizeInBytes = blockSize; + if (blockSize < 32) { + return DRFLAC_FALSE; + } + + if (onMeta) { + drflac_bool32 result = DRFLAC_TRUE; + drflac_uint32 blockSizeRemaining = blockSize; + char* pMime = NULL; + char* pDescription = NULL; + void* pPictureData = NULL; + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.type, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.type = drflac__be2host_32(metadata.data.picture.type); + + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.mimeLength, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.mimeLength = drflac__be2host_32(metadata.data.picture.mimeLength); + + pMime = (char*)drflac__malloc_from_callbacks(metadata.data.picture.mimeLength + 1, pAllocationCallbacks); /* +1 for null terminator. */ + if (pMime == NULL) { + result = DRFLAC_FALSE; + goto done_flac; + } + + if (blockSizeRemaining < metadata.data.picture.mimeLength || onRead(pUserData, pMime, metadata.data.picture.mimeLength) != metadata.data.picture.mimeLength) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= metadata.data.picture.mimeLength; + pMime[metadata.data.picture.mimeLength] = '\0'; /* Null terminate for safety. */ + metadata.data.picture.mime = (const char*)pMime; + + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.descriptionLength, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.descriptionLength = drflac__be2host_32(metadata.data.picture.descriptionLength); + + pDescription = (char*)drflac__malloc_from_callbacks(metadata.data.picture.descriptionLength + 1, pAllocationCallbacks); /* +1 for null terminator. */ + if (pDescription == NULL) { + result = DRFLAC_FALSE; + goto done_flac; + } + + if (blockSizeRemaining < metadata.data.picture.descriptionLength || onRead(pUserData, pDescription, metadata.data.picture.descriptionLength) != metadata.data.picture.descriptionLength) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= metadata.data.picture.descriptionLength; + pDescription[metadata.data.picture.descriptionLength] = '\0'; /* Null terminate for safety. */ + metadata.data.picture.description = (const char*)pDescription; + + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.width, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.width = drflac__be2host_32(metadata.data.picture.width); + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.height, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.height = drflac__be2host_32(metadata.data.picture.height); + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.colorDepth, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.colorDepth = drflac__be2host_32(metadata.data.picture.colorDepth); + + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.indexColorCount, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.indexColorCount = drflac__be2host_32(metadata.data.picture.indexColorCount); + + + /* Picture data. */ + if (blockSizeRemaining < 4 || onRead(pUserData, &metadata.data.picture.pictureDataSize, 4) != 4) { + result = DRFLAC_FALSE; + goto done_flac; + } + blockSizeRemaining -= 4; + metadata.data.picture.pictureDataSize = drflac__be2host_32(metadata.data.picture.pictureDataSize); + + if (blockSizeRemaining < metadata.data.picture.pictureDataSize) { + result = DRFLAC_FALSE; + goto done_flac; + } + + /* For the actual image data we want to store the offset to the start of the stream. */ + metadata.data.picture.pictureDataOffset = runningFilePos + (blockSize - blockSizeRemaining); + + /* + For the allocation of image data, we can allow memory allocation to fail, in which case we just leave + the pointer as null. If it fails, we need to fall back to seeking past the image data. + */ + #ifndef DR_FLAC_NO_PICTURE_METADATA_MALLOC + pPictureData = drflac__malloc_from_callbacks(metadata.data.picture.pictureDataSize, pAllocationCallbacks); + if (pPictureData != NULL) { + if (onRead(pUserData, pPictureData, metadata.data.picture.pictureDataSize) != metadata.data.picture.pictureDataSize) { + result = DRFLAC_FALSE; + goto done_flac; + } + } else + #endif + { + /* Allocation failed. We need to seek past the picture data. */ + if (!onSeek(pUserData, metadata.data.picture.pictureDataSize, DRFLAC_SEEK_CUR)) { + result = DRFLAC_FALSE; + goto done_flac; + } + } + + blockSizeRemaining -= metadata.data.picture.pictureDataSize; + metadata.data.picture.pPictureData = (const drflac_uint8*)pPictureData; + + + /* Only fire the callback if we actually have a way to read the image data. We must have either a valid offset, or a valid data pointer. */ + if (metadata.data.picture.pictureDataOffset != 0 || metadata.data.picture.pPictureData != NULL) { + onMeta(pUserDataMD, &metadata); + } else { + /* Don't have a valid offset or data pointer, so just pretend we don't have a picture metadata. */ + } + + done_flac: + drflac__free_from_callbacks(pMime, pAllocationCallbacks); + drflac__free_from_callbacks(pDescription, pAllocationCallbacks); + drflac__free_from_callbacks(pPictureData, pAllocationCallbacks); + + if (result != DRFLAC_TRUE) { + return DRFLAC_FALSE; + } + } } break; + case DRFLAC_METADATA_BLOCK_TYPE_PADDING: + { + if (onMeta) { + metadata.data.padding.unused = 0; - // These blocks we either don't care about or aren't supporting. - case DRFLAC_BLOCK_TYPE_PADDING: - case DRFLAC_BLOCK_TYPE_INVALID: - default: break; + /* Padding doesn't have anything meaningful in it, so just skip over it, but make sure the caller is aware of it by firing the callback. */ + if (!onSeek(pUserData, blockSize, DRFLAC_SEEK_CUR)) { + isLastBlock = DRFLAC_TRUE; /* An error occurred while seeking. Attempt to recover by treating this as the last block which will in turn terminate the loop. */ + } else { + onMeta(pUserDataMD, &metadata); + } + } + } break; + + case DRFLAC_METADATA_BLOCK_TYPE_INVALID: + { + /* Invalid chunk. Just skip over this one. */ + if (onMeta) { + if (!onSeek(pUserData, blockSize, DRFLAC_SEEK_CUR)) { + isLastBlock = DRFLAC_TRUE; /* An error occurred while seeking. Attempt to recover by treating this as the last block which will in turn terminate the loop. */ + } + } + } break; + + default: + { + /* + It's an unknown chunk, but not necessarily invalid. There's a chance more metadata blocks might be defined later on, so we + can at the very least report the chunk to the application and let it look at the raw data. + */ + if (onMeta) { + void* pRawData = drflac__malloc_from_callbacks(blockSize, pAllocationCallbacks); + if (pRawData != NULL) { + if (onRead(pUserData, pRawData, blockSize) != blockSize) { + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + return DRFLAC_FALSE; + } + } else { + /* Allocation failed. We need to seek past the block. */ + if (!onSeek(pUserData, blockSize, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + } + + metadata.pRawData = pRawData; + metadata.rawDataSize = blockSize; + onMeta(pUserDataMD, &metadata); + + drflac__free_from_callbacks(pRawData, pAllocationCallbacks); + } + } break; } - if (!drflac__seek_bits(&tempFlac, blockSize*8)) { + /* If we're not handling metadata, just skip over the block. If we are, it will have been handled earlier in the switch statement above. */ + if (onMeta == NULL && blockSize > 0) { + if (!onSeek(pUserData, blockSize, DRFLAC_SEEK_CUR)) { + isLastBlock = DRFLAC_TRUE; + } + } + + runningFilePos += blockSize; + if (isLastBlock) { + break; + } + } + + *pSeektablePos = seektablePos; + *pSeekpointCount = seektableSize / DRFLAC_SEEKPOINT_SIZE_IN_BYTES; + *pFirstFramePos = runningFilePos; + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__init_private__native(drflac_init_info* pInit, drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_meta_proc onMeta, void* pUserData, void* pUserDataMD, drflac_bool32 relaxed) +{ + /* Pre Condition: The bit stream should be sitting just past the 4-byte id header. */ + + drflac_uint8 isLastBlock; + drflac_uint8 blockType; + drflac_uint32 blockSize; + + (void)onSeek; + + pInit->container = drflac_container_native; + + /* The first metadata block should be the STREAMINFO block. */ + if (!drflac__read_and_decode_block_header(onRead, pUserData, &isLastBlock, &blockType, &blockSize)) { + return DRFLAC_FALSE; + } + + if (blockType != DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO || blockSize != 34) { + if (!relaxed) { + /* We're opening in strict mode and the first block is not the STREAMINFO block. Error. */ + return DRFLAC_FALSE; + } else { + /* + Relaxed mode. To open from here we need to just find the first frame and set the sample rate, etc. to whatever is defined + for that frame. + */ + pInit->hasStreamInfoBlock = DRFLAC_FALSE; + pInit->hasMetadataBlocks = DRFLAC_FALSE; + + if (!drflac__read_next_flac_frame_header(&pInit->bs, 0, &pInit->firstFrameHeader)) { + return DRFLAC_FALSE; /* Couldn't find a frame. */ + } + + if (pInit->firstFrameHeader.bitsPerSample == 0) { + return DRFLAC_FALSE; /* Failed to initialize because the first frame depends on the STREAMINFO block, which does not exist. */ + } + + pInit->sampleRate = pInit->firstFrameHeader.sampleRate; + pInit->channels = drflac__get_channel_count_from_channel_assignment(pInit->firstFrameHeader.channelAssignment); + pInit->bitsPerSample = pInit->firstFrameHeader.bitsPerSample; + pInit->maxBlockSizeInPCMFrames = 65535; /* <-- See notes here: https://xiph.org/flac/format.html#metadata_block_streaminfo */ + return DRFLAC_TRUE; + } + } else { + drflac_streaminfo streaminfo; + if (!drflac__read_streaminfo(onRead, pUserData, &streaminfo)) { + return DRFLAC_FALSE; + } + + pInit->hasStreamInfoBlock = DRFLAC_TRUE; + pInit->sampleRate = streaminfo.sampleRate; + pInit->channels = streaminfo.channels; + pInit->bitsPerSample = streaminfo.bitsPerSample; + pInit->totalPCMFrameCount = streaminfo.totalPCMFrameCount; + pInit->maxBlockSizeInPCMFrames = streaminfo.maxBlockSizeInPCMFrames; /* Don't care about the min block size - only the max (used for determining the size of the memory allocation). */ + pInit->hasMetadataBlocks = !isLastBlock; + + if (onMeta) { + drflac_metadata metadata; + metadata.type = DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO; + metadata.pRawData = NULL; + metadata.rawDataSize = 0; + metadata.data.streaminfo = streaminfo; + onMeta(pUserDataMD, &metadata); + } + + return DRFLAC_TRUE; + } +} + +#ifndef DR_FLAC_NO_OGG +#define DRFLAC_OGG_MAX_PAGE_SIZE 65307 +#define DRFLAC_OGG_CAPTURE_PATTERN_CRC32 1605413199 /* CRC-32 of "OggS". */ + +typedef enum +{ + drflac_ogg_recover_on_crc_mismatch, + drflac_ogg_fail_on_crc_mismatch +} drflac_ogg_crc_mismatch_recovery; + +#ifndef DR_FLAC_NO_CRC +static drflac_uint32 drflac__crc32_table[] = { + 0x00000000L, 0x04C11DB7L, 0x09823B6EL, 0x0D4326D9L, + 0x130476DCL, 0x17C56B6BL, 0x1A864DB2L, 0x1E475005L, + 0x2608EDB8L, 0x22C9F00FL, 0x2F8AD6D6L, 0x2B4BCB61L, + 0x350C9B64L, 0x31CD86D3L, 0x3C8EA00AL, 0x384FBDBDL, + 0x4C11DB70L, 0x48D0C6C7L, 0x4593E01EL, 0x4152FDA9L, + 0x5F15ADACL, 0x5BD4B01BL, 0x569796C2L, 0x52568B75L, + 0x6A1936C8L, 0x6ED82B7FL, 0x639B0DA6L, 0x675A1011L, + 0x791D4014L, 0x7DDC5DA3L, 0x709F7B7AL, 0x745E66CDL, + 0x9823B6E0L, 0x9CE2AB57L, 0x91A18D8EL, 0x95609039L, + 0x8B27C03CL, 0x8FE6DD8BL, 0x82A5FB52L, 0x8664E6E5L, + 0xBE2B5B58L, 0xBAEA46EFL, 0xB7A96036L, 0xB3687D81L, + 0xAD2F2D84L, 0xA9EE3033L, 0xA4AD16EAL, 0xA06C0B5DL, + 0xD4326D90L, 0xD0F37027L, 0xDDB056FEL, 0xD9714B49L, + 0xC7361B4CL, 0xC3F706FBL, 0xCEB42022L, 0xCA753D95L, + 0xF23A8028L, 0xF6FB9D9FL, 0xFBB8BB46L, 0xFF79A6F1L, + 0xE13EF6F4L, 0xE5FFEB43L, 0xE8BCCD9AL, 0xEC7DD02DL, + 0x34867077L, 0x30476DC0L, 0x3D044B19L, 0x39C556AEL, + 0x278206ABL, 0x23431B1CL, 0x2E003DC5L, 0x2AC12072L, + 0x128E9DCFL, 0x164F8078L, 0x1B0CA6A1L, 0x1FCDBB16L, + 0x018AEB13L, 0x054BF6A4L, 0x0808D07DL, 0x0CC9CDCAL, + 0x7897AB07L, 0x7C56B6B0L, 0x71159069L, 0x75D48DDEL, + 0x6B93DDDBL, 0x6F52C06CL, 0x6211E6B5L, 0x66D0FB02L, + 0x5E9F46BFL, 0x5A5E5B08L, 0x571D7DD1L, 0x53DC6066L, + 0x4D9B3063L, 0x495A2DD4L, 0x44190B0DL, 0x40D816BAL, + 0xACA5C697L, 0xA864DB20L, 0xA527FDF9L, 0xA1E6E04EL, + 0xBFA1B04BL, 0xBB60ADFCL, 0xB6238B25L, 0xB2E29692L, + 0x8AAD2B2FL, 0x8E6C3698L, 0x832F1041L, 0x87EE0DF6L, + 0x99A95DF3L, 0x9D684044L, 0x902B669DL, 0x94EA7B2AL, + 0xE0B41DE7L, 0xE4750050L, 0xE9362689L, 0xEDF73B3EL, + 0xF3B06B3BL, 0xF771768CL, 0xFA325055L, 0xFEF34DE2L, + 0xC6BCF05FL, 0xC27DEDE8L, 0xCF3ECB31L, 0xCBFFD686L, + 0xD5B88683L, 0xD1799B34L, 0xDC3ABDEDL, 0xD8FBA05AL, + 0x690CE0EEL, 0x6DCDFD59L, 0x608EDB80L, 0x644FC637L, + 0x7A089632L, 0x7EC98B85L, 0x738AAD5CL, 0x774BB0EBL, + 0x4F040D56L, 0x4BC510E1L, 0x46863638L, 0x42472B8FL, + 0x5C007B8AL, 0x58C1663DL, 0x558240E4L, 0x51435D53L, + 0x251D3B9EL, 0x21DC2629L, 0x2C9F00F0L, 0x285E1D47L, + 0x36194D42L, 0x32D850F5L, 0x3F9B762CL, 0x3B5A6B9BL, + 0x0315D626L, 0x07D4CB91L, 0x0A97ED48L, 0x0E56F0FFL, + 0x1011A0FAL, 0x14D0BD4DL, 0x19939B94L, 0x1D528623L, + 0xF12F560EL, 0xF5EE4BB9L, 0xF8AD6D60L, 0xFC6C70D7L, + 0xE22B20D2L, 0xE6EA3D65L, 0xEBA91BBCL, 0xEF68060BL, + 0xD727BBB6L, 0xD3E6A601L, 0xDEA580D8L, 0xDA649D6FL, + 0xC423CD6AL, 0xC0E2D0DDL, 0xCDA1F604L, 0xC960EBB3L, + 0xBD3E8D7EL, 0xB9FF90C9L, 0xB4BCB610L, 0xB07DABA7L, + 0xAE3AFBA2L, 0xAAFBE615L, 0xA7B8C0CCL, 0xA379DD7BL, + 0x9B3660C6L, 0x9FF77D71L, 0x92B45BA8L, 0x9675461FL, + 0x8832161AL, 0x8CF30BADL, 0x81B02D74L, 0x857130C3L, + 0x5D8A9099L, 0x594B8D2EL, 0x5408ABF7L, 0x50C9B640L, + 0x4E8EE645L, 0x4A4FFBF2L, 0x470CDD2BL, 0x43CDC09CL, + 0x7B827D21L, 0x7F436096L, 0x7200464FL, 0x76C15BF8L, + 0x68860BFDL, 0x6C47164AL, 0x61043093L, 0x65C52D24L, + 0x119B4BE9L, 0x155A565EL, 0x18197087L, 0x1CD86D30L, + 0x029F3D35L, 0x065E2082L, 0x0B1D065BL, 0x0FDC1BECL, + 0x3793A651L, 0x3352BBE6L, 0x3E119D3FL, 0x3AD08088L, + 0x2497D08DL, 0x2056CD3AL, 0x2D15EBE3L, 0x29D4F654L, + 0xC5A92679L, 0xC1683BCEL, 0xCC2B1D17L, 0xC8EA00A0L, + 0xD6AD50A5L, 0xD26C4D12L, 0xDF2F6BCBL, 0xDBEE767CL, + 0xE3A1CBC1L, 0xE760D676L, 0xEA23F0AFL, 0xEEE2ED18L, + 0xF0A5BD1DL, 0xF464A0AAL, 0xF9278673L, 0xFDE69BC4L, + 0x89B8FD09L, 0x8D79E0BEL, 0x803AC667L, 0x84FBDBD0L, + 0x9ABC8BD5L, 0x9E7D9662L, 0x933EB0BBL, 0x97FFAD0CL, + 0xAFB010B1L, 0xAB710D06L, 0xA6322BDFL, 0xA2F33668L, + 0xBCB4666DL, 0xB8757BDAL, 0xB5365D03L, 0xB1F740B4L +}; +#endif + +static DRFLAC_INLINE drflac_uint32 drflac_crc32_byte(drflac_uint32 crc32, drflac_uint8 data) +{ +#ifndef DR_FLAC_NO_CRC + return (crc32 << 8) ^ drflac__crc32_table[(drflac_uint8)((crc32 >> 24) & 0xFF) ^ data]; +#else + (void)data; + return crc32; +#endif +} + +#if 0 +static DRFLAC_INLINE drflac_uint32 drflac_crc32_uint32(drflac_uint32 crc32, drflac_uint32 data) +{ + crc32 = drflac_crc32_byte(crc32, (drflac_uint8)((data >> 24) & 0xFF)); + crc32 = drflac_crc32_byte(crc32, (drflac_uint8)((data >> 16) & 0xFF)); + crc32 = drflac_crc32_byte(crc32, (drflac_uint8)((data >> 8) & 0xFF)); + crc32 = drflac_crc32_byte(crc32, (drflac_uint8)((data >> 0) & 0xFF)); + return crc32; +} + +static DRFLAC_INLINE drflac_uint32 drflac_crc32_uint64(drflac_uint32 crc32, drflac_uint64 data) +{ + crc32 = drflac_crc32_uint32(crc32, (drflac_uint32)((data >> 32) & 0xFFFFFFFF)); + crc32 = drflac_crc32_uint32(crc32, (drflac_uint32)((data >> 0) & 0xFFFFFFFF)); + return crc32; +} +#endif + +static DRFLAC_INLINE drflac_uint32 drflac_crc32_buffer(drflac_uint32 crc32, drflac_uint8* pData, drflac_uint32 dataSize) +{ + /* This can be optimized. */ + drflac_uint32 i; + for (i = 0; i < dataSize; ++i) { + crc32 = drflac_crc32_byte(crc32, pData[i]); + } + return crc32; +} + + +static DRFLAC_INLINE drflac_bool32 drflac_ogg__is_capture_pattern(drflac_uint8 pattern[4]) +{ + return pattern[0] == 'O' && pattern[1] == 'g' && pattern[2] == 'g' && pattern[3] == 'S'; +} + +static DRFLAC_INLINE drflac_uint32 drflac_ogg__get_page_header_size(drflac_ogg_page_header* pHeader) +{ + return 27 + pHeader->segmentCount; +} + +static DRFLAC_INLINE drflac_uint32 drflac_ogg__get_page_body_size(drflac_ogg_page_header* pHeader) +{ + drflac_uint32 pageBodySize = 0; + int i; + + for (i = 0; i < pHeader->segmentCount; ++i) { + pageBodySize += pHeader->segmentTable[i]; + } + + return pageBodySize; +} + +static drflac_result drflac_ogg__read_page_header_after_capture_pattern(drflac_read_proc onRead, void* pUserData, drflac_ogg_page_header* pHeader, drflac_uint32* pBytesRead, drflac_uint32* pCRC32) +{ + drflac_uint8 data[23]; + drflac_uint32 i; + + DRFLAC_ASSERT(*pCRC32 == DRFLAC_OGG_CAPTURE_PATTERN_CRC32); + + if (onRead(pUserData, data, 23) != 23) { + return DRFLAC_AT_END; + } + *pBytesRead += 23; + + /* + It's not actually used, but set the capture pattern to 'OggS' for completeness. Not doing this will cause static analysers to complain about + us trying to access uninitialized data. We could alternatively just comment out this member of the drflac_ogg_page_header structure, but I + like to have it map to the structure of the underlying data. + */ + pHeader->capturePattern[0] = 'O'; + pHeader->capturePattern[1] = 'g'; + pHeader->capturePattern[2] = 'g'; + pHeader->capturePattern[3] = 'S'; + + pHeader->structureVersion = data[0]; + pHeader->headerType = data[1]; + DRFLAC_COPY_MEMORY(&pHeader->granulePosition, &data[ 2], 8); + DRFLAC_COPY_MEMORY(&pHeader->serialNumber, &data[10], 4); + DRFLAC_COPY_MEMORY(&pHeader->sequenceNumber, &data[14], 4); + DRFLAC_COPY_MEMORY(&pHeader->checksum, &data[18], 4); + pHeader->segmentCount = data[22]; + + /* Calculate the CRC. Note that for the calculation the checksum part of the page needs to be set to 0. */ + data[18] = 0; + data[19] = 0; + data[20] = 0; + data[21] = 0; + + for (i = 0; i < 23; ++i) { + *pCRC32 = drflac_crc32_byte(*pCRC32, data[i]); + } + + + if (onRead(pUserData, pHeader->segmentTable, pHeader->segmentCount) != pHeader->segmentCount) { + return DRFLAC_AT_END; + } + *pBytesRead += pHeader->segmentCount; + + for (i = 0; i < pHeader->segmentCount; ++i) { + *pCRC32 = drflac_crc32_byte(*pCRC32, pHeader->segmentTable[i]); + } + + return DRFLAC_SUCCESS; +} + +static drflac_result drflac_ogg__read_page_header(drflac_read_proc onRead, void* pUserData, drflac_ogg_page_header* pHeader, drflac_uint32* pBytesRead, drflac_uint32* pCRC32) +{ + drflac_uint8 id[4]; + + *pBytesRead = 0; + + if (onRead(pUserData, id, 4) != 4) { + return DRFLAC_AT_END; + } + *pBytesRead += 4; + + /* We need to read byte-by-byte until we find the OggS capture pattern. */ + for (;;) { + if (drflac_ogg__is_capture_pattern(id)) { + drflac_result result; + + *pCRC32 = DRFLAC_OGG_CAPTURE_PATTERN_CRC32; + + result = drflac_ogg__read_page_header_after_capture_pattern(onRead, pUserData, pHeader, pBytesRead, pCRC32); + if (result == DRFLAC_SUCCESS) { + return DRFLAC_SUCCESS; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + continue; + } else { + return result; + } + } + } else { + /* The first 4 bytes did not equal the capture pattern. Read the next byte and try again. */ + id[0] = id[1]; + id[1] = id[2]; + id[2] = id[3]; + if (onRead(pUserData, &id[3], 1) != 1) { + return DRFLAC_AT_END; + } + *pBytesRead += 1; + } + } +} + + +/* +The main part of the Ogg encapsulation is the conversion from the physical Ogg bitstream to the native FLAC bitstream. It works +in three general stages: Ogg Physical Bitstream -> Ogg/FLAC Logical Bitstream -> FLAC Native Bitstream. dr_flac is designed +in such a way that the core sections assume everything is delivered in native format. Therefore, for each encapsulation type +dr_flac is supporting there needs to be a layer sitting on top of the onRead and onSeek callbacks that ensures the bits read from +the physical Ogg bitstream are converted and delivered in native FLAC format. +*/ +typedef struct +{ + drflac_read_proc onRead; /* The original onRead callback from drflac_open() and family. */ + drflac_seek_proc onSeek; /* The original onSeek callback from drflac_open() and family. */ + drflac_tell_proc onTell; /* The original onTell callback from drflac_open() and family. */ + void* pUserData; /* The user data passed on onRead and onSeek. This is the user data that was passed on drflac_open() and family. */ + drflac_uint64 currentBytePos; /* The position of the byte we are sitting on in the physical byte stream. Used for efficient seeking. */ + drflac_uint64 firstBytePos; /* The position of the first byte in the physical bitstream. Points to the start of the "OggS" identifier of the FLAC bos page. */ + drflac_uint32 serialNumber; /* The serial number of the FLAC audio pages. This is determined by the initial header page that was read during initialization. */ + drflac_ogg_page_header bosPageHeader; /* Used for seeking. */ + drflac_ogg_page_header currentPageHeader; + drflac_uint32 bytesRemainingInPage; + drflac_uint32 pageDataSize; + drflac_uint8 pageData[DRFLAC_OGG_MAX_PAGE_SIZE]; +} drflac_oggbs; /* oggbs = Ogg Bitstream */ + +static size_t drflac_oggbs__read_physical(drflac_oggbs* oggbs, void* bufferOut, size_t bytesToRead) +{ + size_t bytesActuallyRead = oggbs->onRead(oggbs->pUserData, bufferOut, bytesToRead); + oggbs->currentBytePos += bytesActuallyRead; + + return bytesActuallyRead; +} + +static drflac_bool32 drflac_oggbs__seek_physical(drflac_oggbs* oggbs, drflac_uint64 offset, drflac_seek_origin origin) +{ + if (origin == DRFLAC_SEEK_SET) { + if (offset <= 0x7FFFFFFF) { + if (!oggbs->onSeek(oggbs->pUserData, (int)offset, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + oggbs->currentBytePos = offset; + + return DRFLAC_TRUE; + } else { + if (!oggbs->onSeek(oggbs->pUserData, 0x7FFFFFFF, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + oggbs->currentBytePos = offset; + + return drflac_oggbs__seek_physical(oggbs, offset - 0x7FFFFFFF, DRFLAC_SEEK_CUR); + } + } else { + while (offset > 0x7FFFFFFF) { + if (!oggbs->onSeek(oggbs->pUserData, 0x7FFFFFFF, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + oggbs->currentBytePos += 0x7FFFFFFF; + offset -= 0x7FFFFFFF; + } + + if (!oggbs->onSeek(oggbs->pUserData, (int)offset, DRFLAC_SEEK_CUR)) { /* <-- Safe cast thanks to the loop above. */ + return DRFLAC_FALSE; + } + oggbs->currentBytePos += offset; + + return DRFLAC_TRUE; + } +} + +static drflac_bool32 drflac_oggbs__goto_next_page(drflac_oggbs* oggbs, drflac_ogg_crc_mismatch_recovery recoveryMethod) +{ + drflac_ogg_page_header header; + for (;;) { + drflac_uint32 crc32 = 0; + drflac_uint32 bytesRead; + drflac_uint32 pageBodySize; +#ifndef DR_FLAC_NO_CRC + drflac_uint32 actualCRC32; +#endif + + if (drflac_ogg__read_page_header(oggbs->onRead, oggbs->pUserData, &header, &bytesRead, &crc32) != DRFLAC_SUCCESS) { + return DRFLAC_FALSE; + } + oggbs->currentBytePos += bytesRead; + + pageBodySize = drflac_ogg__get_page_body_size(&header); + if (pageBodySize > DRFLAC_OGG_MAX_PAGE_SIZE) { + continue; /* Invalid page size. Assume it's corrupted and just move to the next page. */ + } + + if (header.serialNumber != oggbs->serialNumber) { + /* It's not a FLAC page. Skip it. */ + if (pageBodySize > 0 && !drflac_oggbs__seek_physical(oggbs, pageBodySize, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + continue; + } + + + /* We need to read the entire page and then do a CRC check on it. If there's a CRC mismatch we need to skip this page. */ + if (drflac_oggbs__read_physical(oggbs, oggbs->pageData, pageBodySize) != pageBodySize) { + return DRFLAC_FALSE; + } + oggbs->pageDataSize = pageBodySize; + +#ifndef DR_FLAC_NO_CRC + actualCRC32 = drflac_crc32_buffer(crc32, oggbs->pageData, oggbs->pageDataSize); + if (actualCRC32 != header.checksum) { + if (recoveryMethod == drflac_ogg_recover_on_crc_mismatch) { + continue; /* CRC mismatch. Skip this page. */ + } else { + /* + Even though we are failing on a CRC mismatch, we still want our stream to be in a good state. Therefore we + go to the next valid page to ensure we're in a good state, but return false to let the caller know that the + seek did not fully complete. + */ + drflac_oggbs__goto_next_page(oggbs, drflac_ogg_recover_on_crc_mismatch); + return DRFLAC_FALSE; + } + } +#else + (void)recoveryMethod; /* <-- Silence a warning. */ +#endif + + oggbs->currentPageHeader = header; + oggbs->bytesRemainingInPage = pageBodySize; + return DRFLAC_TRUE; + } +} + +/* Function below is unused at the moment, but I might be re-adding it later. */ +#if 0 +static drflac_uint8 drflac_oggbs__get_current_segment_index(drflac_oggbs* oggbs, drflac_uint8* pBytesRemainingInSeg) +{ + drflac_uint32 bytesConsumedInPage = drflac_ogg__get_page_body_size(&oggbs->currentPageHeader) - oggbs->bytesRemainingInPage; + drflac_uint8 iSeg = 0; + drflac_uint32 iByte = 0; + while (iByte < bytesConsumedInPage) { + drflac_uint8 segmentSize = oggbs->currentPageHeader.segmentTable[iSeg]; + if (iByte + segmentSize > bytesConsumedInPage) { + break; + } else { + iSeg += 1; + iByte += segmentSize; + } + } + + *pBytesRemainingInSeg = oggbs->currentPageHeader.segmentTable[iSeg] - (drflac_uint8)(bytesConsumedInPage - iByte); + return iSeg; +} + +static drflac_bool32 drflac_oggbs__seek_to_next_packet(drflac_oggbs* oggbs) +{ + /* The current packet ends when we get to the segment with a lacing value of < 255 which is not at the end of a page. */ + for (;;) { + drflac_bool32 atEndOfPage = DRFLAC_FALSE; + + drflac_uint8 bytesRemainingInSeg; + drflac_uint8 iFirstSeg = drflac_oggbs__get_current_segment_index(oggbs, &bytesRemainingInSeg); + + drflac_uint32 bytesToEndOfPacketOrPage = bytesRemainingInSeg; + for (drflac_uint8 iSeg = iFirstSeg; iSeg < oggbs->currentPageHeader.segmentCount; ++iSeg) { + drflac_uint8 segmentSize = oggbs->currentPageHeader.segmentTable[iSeg]; + if (segmentSize < 255) { + if (iSeg == oggbs->currentPageHeader.segmentCount-1) { + atEndOfPage = DRFLAC_TRUE; + } + + break; + } + + bytesToEndOfPacketOrPage += segmentSize; + } + + /* + At this point we will have found either the packet or the end of the page. If were at the end of the page we'll + want to load the next page and keep searching for the end of the packet. + */ + drflac_oggbs__seek_physical(oggbs, bytesToEndOfPacketOrPage, DRFLAC_SEEK_CUR); + oggbs->bytesRemainingInPage -= bytesToEndOfPacketOrPage; + + if (atEndOfPage) { + /* + We're potentially at the next packet, but we need to check the next page first to be sure because the packet may + straddle pages. + */ + if (!drflac_oggbs__goto_next_page(oggbs)) { + return DRFLAC_FALSE; + } + + /* If it's a fresh packet it most likely means we're at the next packet. */ + if ((oggbs->currentPageHeader.headerType & 0x01) == 0) { + return DRFLAC_TRUE; + } + } else { + /* We're at the next packet. */ + return DRFLAC_TRUE; + } + } +} + +static drflac_bool32 drflac_oggbs__seek_to_next_frame(drflac_oggbs* oggbs) +{ + /* The bitstream should be sitting on the first byte just after the header of the frame. */ + + /* What we're actually doing here is seeking to the start of the next packet. */ + return drflac_oggbs__seek_to_next_packet(oggbs); +} +#endif + +static size_t drflac__on_read_ogg(void* pUserData, void* bufferOut, size_t bytesToRead) +{ + drflac_oggbs* oggbs = (drflac_oggbs*)pUserData; + drflac_uint8* pRunningBufferOut = (drflac_uint8*)bufferOut; + size_t bytesRead = 0; + + DRFLAC_ASSERT(oggbs != NULL); + DRFLAC_ASSERT(pRunningBufferOut != NULL); + + /* Reading is done page-by-page. If we've run out of bytes in the page we need to move to the next one. */ + while (bytesRead < bytesToRead) { + size_t bytesRemainingToRead = bytesToRead - bytesRead; + + if (oggbs->bytesRemainingInPage >= bytesRemainingToRead) { + DRFLAC_COPY_MEMORY(pRunningBufferOut, oggbs->pageData + (oggbs->pageDataSize - oggbs->bytesRemainingInPage), bytesRemainingToRead); + bytesRead += bytesRemainingToRead; + oggbs->bytesRemainingInPage -= (drflac_uint32)bytesRemainingToRead; + break; + } + + /* If we get here it means some of the requested data is contained in the next pages. */ + if (oggbs->bytesRemainingInPage > 0) { + DRFLAC_COPY_MEMORY(pRunningBufferOut, oggbs->pageData + (oggbs->pageDataSize - oggbs->bytesRemainingInPage), oggbs->bytesRemainingInPage); + bytesRead += oggbs->bytesRemainingInPage; + pRunningBufferOut += oggbs->bytesRemainingInPage; + oggbs->bytesRemainingInPage = 0; + } + + DRFLAC_ASSERT(bytesRemainingToRead > 0); + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_recover_on_crc_mismatch)) { + break; /* Failed to go to the next page. Might have simply hit the end of the stream. */ + } + } + + return bytesRead; +} + +static drflac_bool32 drflac__on_seek_ogg(void* pUserData, int offset, drflac_seek_origin origin) +{ + drflac_oggbs* oggbs = (drflac_oggbs*)pUserData; + int bytesSeeked = 0; + + DRFLAC_ASSERT(oggbs != NULL); + DRFLAC_ASSERT(offset >= 0); /* <-- Never seek backwards. */ + + /* Seeking is always forward which makes things a lot simpler. */ + if (origin == DRFLAC_SEEK_SET) { + if (!drflac_oggbs__seek_physical(oggbs, (int)oggbs->firstBytePos, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_fail_on_crc_mismatch)) { + return DRFLAC_FALSE; + } + + return drflac__on_seek_ogg(pUserData, offset, DRFLAC_SEEK_CUR); + } else if (origin == DRFLAC_SEEK_CUR) { + while (bytesSeeked < offset) { + int bytesRemainingToSeek = offset - bytesSeeked; + DRFLAC_ASSERT(bytesRemainingToSeek >= 0); + + if (oggbs->bytesRemainingInPage >= (size_t)bytesRemainingToSeek) { + bytesSeeked += bytesRemainingToSeek; + (void)bytesSeeked; /* <-- Silence a dead store warning emitted by Clang Static Analyzer. */ + oggbs->bytesRemainingInPage -= bytesRemainingToSeek; + break; + } + + /* If we get here it means some of the requested data is contained in the next pages. */ + if (oggbs->bytesRemainingInPage > 0) { + bytesSeeked += (int)oggbs->bytesRemainingInPage; + oggbs->bytesRemainingInPage = 0; + } + + DRFLAC_ASSERT(bytesRemainingToSeek > 0); + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_fail_on_crc_mismatch)) { + /* Failed to go to the next page. We either hit the end of the stream or had a CRC mismatch. */ + return DRFLAC_FALSE; + } + } + } else if (origin == DRFLAC_SEEK_END) { + /* Seeking to the end is not supported. */ + return DRFLAC_FALSE; + } + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__on_tell_ogg(void* pUserData, drflac_int64* pCursor) +{ + /* + Not implemented for Ogg containers because we don't currently track the byte position of the logical bitstream. To support this, we'll need + to track the position in drflac__on_read_ogg and drflac__on_seek_ogg. + */ + (void)pUserData; + (void)pCursor; + return DRFLAC_FALSE; +} + + +static drflac_bool32 drflac_ogg__seek_to_pcm_frame(drflac* pFlac, drflac_uint64 pcmFrameIndex) +{ + drflac_oggbs* oggbs = (drflac_oggbs*)pFlac->_oggbs; + drflac_uint64 originalBytePos; + drflac_uint64 runningGranulePosition; + drflac_uint64 runningFrameBytePos; + drflac_uint64 runningPCMFrameCount; + + DRFLAC_ASSERT(oggbs != NULL); + + originalBytePos = oggbs->currentBytePos; /* For recovery. Points to the OggS identifier. */ + + /* First seek to the first frame. */ + if (!drflac__seek_to_byte(&pFlac->bs, pFlac->firstFLACFramePosInBytes)) { + return DRFLAC_FALSE; + } + oggbs->bytesRemainingInPage = 0; + + runningGranulePosition = 0; + for (;;) { + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_recover_on_crc_mismatch)) { + drflac_oggbs__seek_physical(oggbs, originalBytePos, DRFLAC_SEEK_SET); + return DRFLAC_FALSE; /* Never did find that sample... */ + } + + runningFrameBytePos = oggbs->currentBytePos - drflac_ogg__get_page_header_size(&oggbs->currentPageHeader) - oggbs->pageDataSize; + if (oggbs->currentPageHeader.granulePosition >= pcmFrameIndex) { + break; /* The sample is somewhere in the previous page. */ + } + + /* + At this point we know the sample is not in the previous page. It could possibly be in this page. For simplicity we + disregard any pages that do not begin a fresh packet. + */ + if ((oggbs->currentPageHeader.headerType & 0x01) == 0) { /* <-- Is it a fresh page? */ + if (oggbs->currentPageHeader.segmentTable[0] >= 2) { + drflac_uint8 firstBytesInPage[2]; + firstBytesInPage[0] = oggbs->pageData[0]; + firstBytesInPage[1] = oggbs->pageData[1]; + + if ((firstBytesInPage[0] == 0xFF) && (firstBytesInPage[1] & 0xFC) == 0xF8) { /* <-- Does the page begin with a frame's sync code? */ + runningGranulePosition = oggbs->currentPageHeader.granulePosition; + } + + continue; + } + } + } + + /* + We found the page that that is closest to the sample, so now we need to find it. The first thing to do is seek to the + start of that page. In the loop above we checked that it was a fresh page which means this page is also the start of + a new frame. This property means that after we've seeked to the page we can immediately start looping over frames until + we find the one containing the target sample. + */ + if (!drflac_oggbs__seek_physical(oggbs, runningFrameBytePos, DRFLAC_SEEK_SET)) { + return DRFLAC_FALSE; + } + if (!drflac_oggbs__goto_next_page(oggbs, drflac_ogg_recover_on_crc_mismatch)) { + return DRFLAC_FALSE; + } + + /* + At this point we'll be sitting on the first byte of the frame header of the first frame in the page. We just keep + looping over these frames until we find the one containing the sample we're after. + */ + runningPCMFrameCount = runningGranulePosition; + for (;;) { + /* + There are two ways to find the sample and seek past irrelevant frames: + 1) Use the native FLAC decoder. + 2) Use Ogg's framing system. + + Both of these options have their own pros and cons. Using the native FLAC decoder is slower because it needs to + do a full decode of the frame. Using Ogg's framing system is faster, but more complicated and involves some code + duplication for the decoding of frame headers. + + Another thing to consider is that using the Ogg framing system will perform direct seeking of the physical Ogg + bitstream. This is important to consider because it means we cannot read data from the drflac_bs object using the + standard drflac__*() APIs because that will read in extra data for its own internal caching which in turn breaks + the positioning of the read pointer of the physical Ogg bitstream. Therefore, anything that would normally be read + using the native FLAC decoding APIs, such as drflac__read_next_flac_frame_header(), need to be re-implemented so as to + avoid the use of the drflac_bs object. + + Considering these issues, I have decided to use the slower native FLAC decoding method for the following reasons: + 1) Seeking is already partially accelerated using Ogg's paging system in the code block above. + 2) Seeking in an Ogg encapsulated FLAC stream is probably quite uncommon. + 3) Simplicity. + */ + drflac_uint64 firstPCMFrameInFLACFrame = 0; + drflac_uint64 lastPCMFrameInFLACFrame = 0; + drflac_uint64 pcmFrameCountInThisFrame; + + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + return DRFLAC_FALSE; + } + + drflac__get_pcm_frame_range_of_current_flac_frame(pFlac, &firstPCMFrameInFLACFrame, &lastPCMFrameInFLACFrame); + + pcmFrameCountInThisFrame = (lastPCMFrameInFLACFrame - firstPCMFrameInFLACFrame) + 1; + + /* If we are seeking to the end of the file and we've just hit it, we're done. */ + if (pcmFrameIndex == pFlac->totalPCMFrameCount && (runningPCMFrameCount + pcmFrameCountInThisFrame) == pFlac->totalPCMFrameCount) { + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + pFlac->currentPCMFrame = pcmFrameIndex; + pFlac->currentFLACFrame.pcmFramesRemaining = 0; + return DRFLAC_TRUE; + } else { + return DRFLAC_FALSE; + } + } + + if (pcmFrameIndex < (runningPCMFrameCount + pcmFrameCountInThisFrame)) { + /* + The sample should be in this FLAC frame. We need to fully decode it, however if it's an invalid frame (a CRC mismatch), we need to pretend + it never existed and keep iterating. + */ + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + /* The frame is valid. We just need to skip over some samples to ensure it's sample-exact. */ + drflac_uint64 pcmFramesToDecode = (size_t)(pcmFrameIndex - runningPCMFrameCount); /* <-- Safe cast because the maximum number of samples in a frame is 65535. */ + if (pcmFramesToDecode == 0) { + return DRFLAC_TRUE; + } + + pFlac->currentPCMFrame = runningPCMFrameCount; + + return drflac__seek_forward_by_pcm_frames(pFlac, pcmFramesToDecode) == pcmFramesToDecode; /* <-- If this fails, something bad has happened (it should never fail). */ + } else { + if (result == DRFLAC_CRC_MISMATCH) { + continue; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } else { + /* + It's not in this frame. We need to seek past the frame, but check if there was a CRC mismatch. If so, we pretend this + frame never existed and leave the running sample count untouched. + */ + drflac_result result = drflac__seek_to_next_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + runningPCMFrameCount += pcmFrameCountInThisFrame; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + continue; /* CRC mismatch. Pretend this frame never existed. */ + } else { + return DRFLAC_FALSE; + } + } + } + } +} + + + +static drflac_bool32 drflac__init_private__ogg(drflac_init_info* pInit, drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_meta_proc onMeta, void* pUserData, void* pUserDataMD, drflac_bool32 relaxed) +{ + drflac_ogg_page_header header; + drflac_uint32 crc32 = DRFLAC_OGG_CAPTURE_PATTERN_CRC32; + drflac_uint32 bytesRead = 0; + + /* Pre Condition: The bit stream should be sitting just past the 4-byte OggS capture pattern. */ + (void)relaxed; + + pInit->container = drflac_container_ogg; + pInit->oggFirstBytePos = 0; + + /* + We'll get here if the first 4 bytes of the stream were the OggS capture pattern, however it doesn't necessarily mean the + stream includes FLAC encoded audio. To check for this we need to scan the beginning-of-stream page markers and check if + any match the FLAC specification. Important to keep in mind that the stream may be multiplexed. + */ + if (drflac_ogg__read_page_header_after_capture_pattern(onRead, pUserData, &header, &bytesRead, &crc32) != DRFLAC_SUCCESS) { + return DRFLAC_FALSE; + } + pInit->runningFilePos += bytesRead; + + for (;;) { + int pageBodySize; + + /* Break if we're past the beginning of stream page. */ + if ((header.headerType & 0x02) == 0) { + return DRFLAC_FALSE; + } + + /* Check if it's a FLAC header. */ + pageBodySize = drflac_ogg__get_page_body_size(&header); + if (pageBodySize == 51) { /* 51 = the lacing value of the FLAC header packet. */ + /* It could be a FLAC page... */ + drflac_uint32 bytesRemainingInPage = pageBodySize; + drflac_uint8 packetType; + + if (onRead(pUserData, &packetType, 1) != 1) { + return DRFLAC_FALSE; + } + + bytesRemainingInPage -= 1; + if (packetType == 0x7F) { + /* Increasingly more likely to be a FLAC page... */ + drflac_uint8 sig[4]; + if (onRead(pUserData, sig, 4) != 4) { + return DRFLAC_FALSE; + } + + bytesRemainingInPage -= 4; + if (sig[0] == 'F' && sig[1] == 'L' && sig[2] == 'A' && sig[3] == 'C') { + /* Almost certainly a FLAC page... */ + drflac_uint8 mappingVersion[2]; + if (onRead(pUserData, mappingVersion, 2) != 2) { + return DRFLAC_FALSE; + } + + if (mappingVersion[0] != 1) { + return DRFLAC_FALSE; /* Only supporting version 1.x of the Ogg mapping. */ + } + + /* + The next 2 bytes are the non-audio packets, not including this one. We don't care about this because we're going to + be handling it in a generic way based on the serial number and packet types. + */ + if (!onSeek(pUserData, 2, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + + /* Expecting the native FLAC signature "fLaC". */ + if (onRead(pUserData, sig, 4) != 4) { + return DRFLAC_FALSE; + } + + if (sig[0] == 'f' && sig[1] == 'L' && sig[2] == 'a' && sig[3] == 'C') { + /* The remaining data in the page should be the STREAMINFO block. */ + drflac_streaminfo streaminfo; + drflac_uint8 isLastBlock; + drflac_uint8 blockType; + drflac_uint32 blockSize; + if (!drflac__read_and_decode_block_header(onRead, pUserData, &isLastBlock, &blockType, &blockSize)) { + return DRFLAC_FALSE; + } + + if (blockType != DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO || blockSize != 34) { + return DRFLAC_FALSE; /* Invalid block type. First block must be the STREAMINFO block. */ + } + + if (drflac__read_streaminfo(onRead, pUserData, &streaminfo)) { + /* Success! */ + pInit->hasStreamInfoBlock = DRFLAC_TRUE; + pInit->sampleRate = streaminfo.sampleRate; + pInit->channels = streaminfo.channels; + pInit->bitsPerSample = streaminfo.bitsPerSample; + pInit->totalPCMFrameCount = streaminfo.totalPCMFrameCount; + pInit->maxBlockSizeInPCMFrames = streaminfo.maxBlockSizeInPCMFrames; + pInit->hasMetadataBlocks = !isLastBlock; + + if (onMeta) { + drflac_metadata metadata; + metadata.type = DRFLAC_METADATA_BLOCK_TYPE_STREAMINFO; + metadata.pRawData = NULL; + metadata.rawDataSize = 0; + metadata.data.streaminfo = streaminfo; + onMeta(pUserDataMD, &metadata); + } + + pInit->runningFilePos += pageBodySize; + pInit->oggFirstBytePos = pInit->runningFilePos - 79; /* Subtracting 79 will place us right on top of the "OggS" identifier of the FLAC bos page. */ + pInit->oggSerial = header.serialNumber; + pInit->oggBosHeader = header; + break; + } else { + /* Failed to read STREAMINFO block. Aww, so close... */ + return DRFLAC_FALSE; + } + } else { + /* Invalid file. */ + return DRFLAC_FALSE; + } + } else { + /* Not a FLAC header. Skip it. */ + if (!onSeek(pUserData, bytesRemainingInPage, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + } + } else { + /* Not a FLAC header. Seek past the entire page and move on to the next. */ + if (!onSeek(pUserData, bytesRemainingInPage, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + } + } else { + if (!onSeek(pUserData, pageBodySize, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; + } + } + + pInit->runningFilePos += pageBodySize; + + + /* Read the header of the next page. */ + if (drflac_ogg__read_page_header(onRead, pUserData, &header, &bytesRead, &crc32) != DRFLAC_SUCCESS) { + return DRFLAC_FALSE; + } + pInit->runningFilePos += bytesRead; + } + + /* + If we get here it means we found a FLAC audio stream. We should be sitting on the first byte of the header of the next page. The next + packets in the FLAC logical stream contain the metadata. The only thing left to do in the initialization phase for Ogg is to create the + Ogg bistream object. + */ + pInit->hasMetadataBlocks = DRFLAC_TRUE; /* <-- Always have at least VORBIS_COMMENT metadata block. */ + return DRFLAC_TRUE; +} +#endif + +static drflac_bool32 drflac__init_private(drflac_init_info* pInit, drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, drflac_container container, void* pUserData, void* pUserDataMD) +{ + drflac_bool32 relaxed; + drflac_uint8 id[4]; + + if (pInit == NULL || onRead == NULL || onSeek == NULL) { /* <-- onTell is optional. */ + return DRFLAC_FALSE; + } + + DRFLAC_ZERO_MEMORY(pInit, sizeof(*pInit)); + pInit->onRead = onRead; + pInit->onSeek = onSeek; + pInit->onTell = onTell; + pInit->onMeta = onMeta; + pInit->container = container; + pInit->pUserData = pUserData; + pInit->pUserDataMD = pUserDataMD; + + pInit->bs.onRead = onRead; + pInit->bs.onSeek = onSeek; + pInit->bs.onTell = onTell; + pInit->bs.pUserData = pUserData; + drflac__reset_cache(&pInit->bs); + + + /* If the container is explicitly defined then we can try opening in relaxed mode. */ + relaxed = container != drflac_container_unknown; + + /* Skip over any ID3 tags. */ + for (;;) { + if (onRead(pUserData, id, 4) != 4) { + return DRFLAC_FALSE; /* Ran out of data. */ + } + pInit->runningFilePos += 4; + + if (id[0] == 'I' && id[1] == 'D' && id[2] == '3') { + drflac_uint8 header[6]; + drflac_uint8 flags; + drflac_uint32 headerSize; + + if (onRead(pUserData, header, 6) != 6) { + return DRFLAC_FALSE; /* Ran out of data. */ + } + pInit->runningFilePos += 6; + + flags = header[1]; + + DRFLAC_COPY_MEMORY(&headerSize, header+2, 4); + headerSize = drflac__unsynchsafe_32(drflac__be2host_32(headerSize)); + if (flags & 0x10) { + headerSize += 10; + } + + if (!onSeek(pUserData, headerSize, DRFLAC_SEEK_CUR)) { + return DRFLAC_FALSE; /* Failed to seek past the tag. */ + } + pInit->runningFilePos += headerSize; + } else { + break; + } + } + + if (id[0] == 'f' && id[1] == 'L' && id[2] == 'a' && id[3] == 'C') { + return drflac__init_private__native(pInit, onRead, onSeek, onMeta, pUserData, pUserDataMD, relaxed); + } +#ifndef DR_FLAC_NO_OGG + if (id[0] == 'O' && id[1] == 'g' && id[2] == 'g' && id[3] == 'S') { + return drflac__init_private__ogg(pInit, onRead, onSeek, onMeta, pUserData, pUserDataMD, relaxed); + } +#endif + + /* If we get here it means we likely don't have a header. Try opening in relaxed mode, if applicable. */ + if (relaxed) { + if (container == drflac_container_native) { + return drflac__init_private__native(pInit, onRead, onSeek, onMeta, pUserData, pUserDataMD, relaxed); + } +#ifndef DR_FLAC_NO_OGG + if (container == drflac_container_ogg) { + return drflac__init_private__ogg(pInit, onRead, onSeek, onMeta, pUserData, pUserDataMD, relaxed); + } +#endif + } + + /* Unsupported container. */ + return DRFLAC_FALSE; +} + +static void drflac__init_from_info(drflac* pFlac, const drflac_init_info* pInit) +{ + DRFLAC_ASSERT(pFlac != NULL); + DRFLAC_ASSERT(pInit != NULL); + + DRFLAC_ZERO_MEMORY(pFlac, sizeof(*pFlac)); + pFlac->bs = pInit->bs; + pFlac->onMeta = pInit->onMeta; + pFlac->pUserDataMD = pInit->pUserDataMD; + pFlac->maxBlockSizeInPCMFrames = pInit->maxBlockSizeInPCMFrames; + pFlac->sampleRate = pInit->sampleRate; + pFlac->channels = (drflac_uint8)pInit->channels; + pFlac->bitsPerSample = (drflac_uint8)pInit->bitsPerSample; + pFlac->totalPCMFrameCount = pInit->totalPCMFrameCount; + pFlac->container = pInit->container; +} + + +static drflac* drflac_open_with_metadata_private(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, drflac_container container, void* pUserData, void* pUserDataMD, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac_init_info init; + drflac_uint32 allocationSize; + drflac_uint32 wholeSIMDVectorCountPerChannel; + drflac_uint32 decodedSamplesAllocationSize; +#ifndef DR_FLAC_NO_OGG + drflac_oggbs* pOggbs = NULL; +#endif + drflac_uint64 firstFramePos; + drflac_uint64 seektablePos; + drflac_uint32 seekpointCount; + drflac_allocation_callbacks allocationCallbacks; + drflac* pFlac; + + /* CPU support first. */ + drflac__init_cpu_caps(); + + if (!drflac__init_private(&init, onRead, onSeek, onTell, onMeta, container, pUserData, pUserDataMD)) { + return NULL; + } + + if (pAllocationCallbacks != NULL) { + allocationCallbacks = *pAllocationCallbacks; + if (allocationCallbacks.onFree == NULL || (allocationCallbacks.onMalloc == NULL && allocationCallbacks.onRealloc == NULL)) { + return NULL; /* Invalid allocation callbacks. */ + } + } else { + allocationCallbacks.pUserData = NULL; + allocationCallbacks.onMalloc = drflac__malloc_default; + allocationCallbacks.onRealloc = drflac__realloc_default; + allocationCallbacks.onFree = drflac__free_default; + } + + + /* + The size of the allocation for the drflac object needs to be large enough to fit the following: + 1) The main members of the drflac structure + 2) A block of memory large enough to store the decoded samples of the largest frame in the stream + 3) If the container is Ogg, a drflac_oggbs object + + The complicated part of the allocation is making sure there's enough room the decoded samples, taking into consideration + the different SIMD instruction sets. + */ + allocationSize = sizeof(drflac); + + /* + The allocation size for decoded frames depends on the number of 32-bit integers that fit inside the largest SIMD vector + we are supporting. + */ + if ((init.maxBlockSizeInPCMFrames % (DRFLAC_MAX_SIMD_VECTOR_SIZE / sizeof(drflac_int32))) == 0) { + wholeSIMDVectorCountPerChannel = (init.maxBlockSizeInPCMFrames / (DRFLAC_MAX_SIMD_VECTOR_SIZE / sizeof(drflac_int32))); + } else { + wholeSIMDVectorCountPerChannel = (init.maxBlockSizeInPCMFrames / (DRFLAC_MAX_SIMD_VECTOR_SIZE / sizeof(drflac_int32))) + 1; + } + + decodedSamplesAllocationSize = wholeSIMDVectorCountPerChannel * DRFLAC_MAX_SIMD_VECTOR_SIZE * init.channels; + + allocationSize += decodedSamplesAllocationSize; + allocationSize += DRFLAC_MAX_SIMD_VECTOR_SIZE; /* Allocate extra bytes to ensure we have enough for alignment. */ + +#ifndef DR_FLAC_NO_OGG + /* There's additional data required for Ogg streams. */ + if (init.container == drflac_container_ogg) { + allocationSize += sizeof(drflac_oggbs); + + pOggbs = (drflac_oggbs*)drflac__malloc_from_callbacks(sizeof(*pOggbs), &allocationCallbacks); + if (pOggbs == NULL) { + return NULL; /*DRFLAC_OUT_OF_MEMORY;*/ + } + + DRFLAC_ZERO_MEMORY(pOggbs, sizeof(*pOggbs)); + pOggbs->onRead = onRead; + pOggbs->onSeek = onSeek; + pOggbs->onTell = onTell; + pOggbs->pUserData = pUserData; + pOggbs->currentBytePos = init.oggFirstBytePos; + pOggbs->firstBytePos = init.oggFirstBytePos; + pOggbs->serialNumber = init.oggSerial; + pOggbs->bosPageHeader = init.oggBosHeader; + pOggbs->bytesRemainingInPage = 0; + } +#endif + + /* + This part is a bit awkward. We need to load the seektable so that it can be referenced in-memory, but I want the drflac object to + consist of only a single heap allocation. To this, the size of the seek table needs to be known, which we determine when reading + and decoding the metadata. + */ + firstFramePos = 42; /* <-- We know we are at byte 42 at this point. */ + seektablePos = 0; + seekpointCount = 0; + if (init.hasMetadataBlocks) { + drflac_read_proc onReadOverride = onRead; + drflac_seek_proc onSeekOverride = onSeek; + drflac_tell_proc onTellOverride = onTell; + void* pUserDataOverride = pUserData; + +#ifndef DR_FLAC_NO_OGG + if (init.container == drflac_container_ogg) { + onReadOverride = drflac__on_read_ogg; + onSeekOverride = drflac__on_seek_ogg; + onTellOverride = drflac__on_tell_ogg; + pUserDataOverride = (void*)pOggbs; + } +#endif + + if (!drflac__read_and_decode_metadata(onReadOverride, onSeekOverride, onTellOverride, onMeta, pUserDataOverride, pUserDataMD, &firstFramePos, &seektablePos, &seekpointCount, &allocationCallbacks)) { + #ifndef DR_FLAC_NO_OGG + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + #endif return NULL; } + + allocationSize += seekpointCount * sizeof(drflac_seekpoint); + } + + + pFlac = (drflac*)drflac__malloc_from_callbacks(allocationSize, &allocationCallbacks); + if (pFlac == NULL) { + #ifndef DR_FLAC_NO_OGG + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + #endif + return NULL; + } + + drflac__init_from_info(pFlac, &init); + pFlac->allocationCallbacks = allocationCallbacks; + pFlac->pDecodedSamples = (drflac_int32*)drflac_align((size_t)pFlac->pExtraData, DRFLAC_MAX_SIMD_VECTOR_SIZE); + +#ifndef DR_FLAC_NO_OGG + if (init.container == drflac_container_ogg) { + drflac_oggbs* pInternalOggbs = (drflac_oggbs*)((drflac_uint8*)pFlac->pDecodedSamples + decodedSamplesAllocationSize + (seekpointCount * sizeof(drflac_seekpoint))); + DRFLAC_COPY_MEMORY(pInternalOggbs, pOggbs, sizeof(*pOggbs)); + + /* At this point the pOggbs object has been handed over to pInternalOggbs and can be freed. */ + drflac__free_from_callbacks(pOggbs, &allocationCallbacks); + pOggbs = NULL; + + /* The Ogg bistream needs to be layered on top of the original bitstream. */ + pFlac->bs.onRead = drflac__on_read_ogg; + pFlac->bs.onSeek = drflac__on_seek_ogg; + pFlac->bs.onTell = drflac__on_tell_ogg; + pFlac->bs.pUserData = (void*)pInternalOggbs; + pFlac->_oggbs = (void*)pInternalOggbs; + } +#endif + + pFlac->firstFLACFramePosInBytes = firstFramePos; + + /* NOTE: Seektables are not currently compatible with Ogg encapsulation (Ogg has its own accelerated seeking system). I may change this later, so I'm leaving this here for now. */ +#ifndef DR_FLAC_NO_OGG + if (init.container == drflac_container_ogg) + { + pFlac->pSeekpoints = NULL; + pFlac->seekpointCount = 0; + } + else +#endif + { + /* If we have a seektable we need to load it now, making sure we move back to where we were previously. */ + if (seektablePos != 0) { + pFlac->seekpointCount = seekpointCount; + pFlac->pSeekpoints = (drflac_seekpoint*)((drflac_uint8*)pFlac->pDecodedSamples + decodedSamplesAllocationSize); + + DRFLAC_ASSERT(pFlac->bs.onSeek != NULL); + DRFLAC_ASSERT(pFlac->bs.onRead != NULL); + + /* Seek to the seektable, then just read directly into our seektable buffer. */ + if (pFlac->bs.onSeek(pFlac->bs.pUserData, (int)seektablePos, DRFLAC_SEEK_SET)) { + drflac_uint32 iSeekpoint; + + for (iSeekpoint = 0; iSeekpoint < seekpointCount; iSeekpoint += 1) { + if (pFlac->bs.onRead(pFlac->bs.pUserData, pFlac->pSeekpoints + iSeekpoint, DRFLAC_SEEKPOINT_SIZE_IN_BYTES) == DRFLAC_SEEKPOINT_SIZE_IN_BYTES) { + /* Endian swap. */ + pFlac->pSeekpoints[iSeekpoint].firstPCMFrame = drflac__be2host_64(pFlac->pSeekpoints[iSeekpoint].firstPCMFrame); + pFlac->pSeekpoints[iSeekpoint].flacFrameOffset = drflac__be2host_64(pFlac->pSeekpoints[iSeekpoint].flacFrameOffset); + pFlac->pSeekpoints[iSeekpoint].pcmFrameCount = drflac__be2host_16(pFlac->pSeekpoints[iSeekpoint].pcmFrameCount); + } else { + /* Failed to read the seektable. Pretend we don't have one. */ + pFlac->pSeekpoints = NULL; + pFlac->seekpointCount = 0; + break; + } + } + + /* We need to seek back to where we were. If this fails it's a critical error. */ + if (!pFlac->bs.onSeek(pFlac->bs.pUserData, (int)pFlac->firstFLACFramePosInBytes, DRFLAC_SEEK_SET)) { + drflac__free_from_callbacks(pFlac, &allocationCallbacks); + return NULL; + } + } else { + /* Failed to seek to the seektable. Ominous sign, but for now we can just pretend we don't have one. */ + pFlac->pSeekpoints = NULL; + pFlac->seekpointCount = 0; + } + } } - // At this point we should be sitting right at the start of the very first frame. - tempFlac.firstFramePos = drflac__tell(&tempFlac); - - drflac* pFlac = (drflac*)malloc(sizeof(*pFlac) - sizeof(pFlac->pExtraData) + (tempFlac.maxBlockSize * tempFlac.channels * sizeof(int32_t))); - memcpy(pFlac, &tempFlac, sizeof(tempFlac) - sizeof(pFlac->pExtraData)); - pFlac->pDecodedSamples = (int32_t*)pFlac->pExtraData; + /* + If we get here, but don't have a STREAMINFO block, it means we've opened the stream in relaxed mode and need to decode + the first frame. + */ + if (!init.hasStreamInfoBlock) { + pFlac->currentFLACFrame.header = init.firstFrameHeader; + for (;;) { + drflac_result result = drflac__decode_flac_frame(pFlac); + if (result == DRFLAC_SUCCESS) { + break; + } else { + if (result == DRFLAC_CRC_MISMATCH) { + if (!drflac__read_next_flac_frame_header(&pFlac->bs, pFlac->bitsPerSample, &pFlac->currentFLACFrame.header)) { + drflac__free_from_callbacks(pFlac, &allocationCallbacks); + return NULL; + } + continue; + } else { + drflac__free_from_callbacks(pFlac, &allocationCallbacks); + return NULL; + } + } + } + } return pFlac; } -static void drflac_close(drflac* pFlac) + + +#ifndef DR_FLAC_NO_STDIO +#include +#ifndef DR_FLAC_NO_WCHAR +#include /* For wcslen(), wcsrtombs() */ +#endif + +/* Errno */ +/* drflac_result_from_errno() is only used for fopen() and wfopen() so putting it inside DR_WAV_NO_STDIO for now. If something else needs this later we can move it out. */ +#include +static drflac_result drflac_result_from_errno(int e) +{ + switch (e) + { + case 0: return DRFLAC_SUCCESS; + #ifdef EPERM + case EPERM: return DRFLAC_INVALID_OPERATION; + #endif + #ifdef ENOENT + case ENOENT: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef ESRCH + case ESRCH: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef EINTR + case EINTR: return DRFLAC_INTERRUPT; + #endif + #ifdef EIO + case EIO: return DRFLAC_IO_ERROR; + #endif + #ifdef ENXIO + case ENXIO: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef E2BIG + case E2BIG: return DRFLAC_INVALID_ARGS; + #endif + #ifdef ENOEXEC + case ENOEXEC: return DRFLAC_INVALID_FILE; + #endif + #ifdef EBADF + case EBADF: return DRFLAC_INVALID_FILE; + #endif + #ifdef ECHILD + case ECHILD: return DRFLAC_ERROR; + #endif + #ifdef EAGAIN + case EAGAIN: return DRFLAC_UNAVAILABLE; + #endif + #ifdef ENOMEM + case ENOMEM: return DRFLAC_OUT_OF_MEMORY; + #endif + #ifdef EACCES + case EACCES: return DRFLAC_ACCESS_DENIED; + #endif + #ifdef EFAULT + case EFAULT: return DRFLAC_BAD_ADDRESS; + #endif + #ifdef ENOTBLK + case ENOTBLK: return DRFLAC_ERROR; + #endif + #ifdef EBUSY + case EBUSY: return DRFLAC_BUSY; + #endif + #ifdef EEXIST + case EEXIST: return DRFLAC_ALREADY_EXISTS; + #endif + #ifdef EXDEV + case EXDEV: return DRFLAC_ERROR; + #endif + #ifdef ENODEV + case ENODEV: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef ENOTDIR + case ENOTDIR: return DRFLAC_NOT_DIRECTORY; + #endif + #ifdef EISDIR + case EISDIR: return DRFLAC_IS_DIRECTORY; + #endif + #ifdef EINVAL + case EINVAL: return DRFLAC_INVALID_ARGS; + #endif + #ifdef ENFILE + case ENFILE: return DRFLAC_TOO_MANY_OPEN_FILES; + #endif + #ifdef EMFILE + case EMFILE: return DRFLAC_TOO_MANY_OPEN_FILES; + #endif + #ifdef ENOTTY + case ENOTTY: return DRFLAC_INVALID_OPERATION; + #endif + #ifdef ETXTBSY + case ETXTBSY: return DRFLAC_BUSY; + #endif + #ifdef EFBIG + case EFBIG: return DRFLAC_TOO_BIG; + #endif + #ifdef ENOSPC + case ENOSPC: return DRFLAC_NO_SPACE; + #endif + #ifdef ESPIPE + case ESPIPE: return DRFLAC_BAD_SEEK; + #endif + #ifdef EROFS + case EROFS: return DRFLAC_ACCESS_DENIED; + #endif + #ifdef EMLINK + case EMLINK: return DRFLAC_TOO_MANY_LINKS; + #endif + #ifdef EPIPE + case EPIPE: return DRFLAC_BAD_PIPE; + #endif + #ifdef EDOM + case EDOM: return DRFLAC_OUT_OF_RANGE; + #endif + #ifdef ERANGE + case ERANGE: return DRFLAC_OUT_OF_RANGE; + #endif + #ifdef EDEADLK + case EDEADLK: return DRFLAC_DEADLOCK; + #endif + #ifdef ENAMETOOLONG + case ENAMETOOLONG: return DRFLAC_PATH_TOO_LONG; + #endif + #ifdef ENOLCK + case ENOLCK: return DRFLAC_ERROR; + #endif + #ifdef ENOSYS + case ENOSYS: return DRFLAC_NOT_IMPLEMENTED; + #endif + #if defined(ENOTEMPTY) && ENOTEMPTY != EEXIST /* In AIX, ENOTEMPTY and EEXIST use the same value. */ + case ENOTEMPTY: return DRFLAC_DIRECTORY_NOT_EMPTY; + #endif + #ifdef ELOOP + case ELOOP: return DRFLAC_TOO_MANY_LINKS; + #endif + #ifdef ENOMSG + case ENOMSG: return DRFLAC_NO_MESSAGE; + #endif + #ifdef EIDRM + case EIDRM: return DRFLAC_ERROR; + #endif + #ifdef ECHRNG + case ECHRNG: return DRFLAC_ERROR; + #endif + #ifdef EL2NSYNC + case EL2NSYNC: return DRFLAC_ERROR; + #endif + #ifdef EL3HLT + case EL3HLT: return DRFLAC_ERROR; + #endif + #ifdef EL3RST + case EL3RST: return DRFLAC_ERROR; + #endif + #ifdef ELNRNG + case ELNRNG: return DRFLAC_OUT_OF_RANGE; + #endif + #ifdef EUNATCH + case EUNATCH: return DRFLAC_ERROR; + #endif + #ifdef ENOCSI + case ENOCSI: return DRFLAC_ERROR; + #endif + #ifdef EL2HLT + case EL2HLT: return DRFLAC_ERROR; + #endif + #ifdef EBADE + case EBADE: return DRFLAC_ERROR; + #endif + #ifdef EBADR + case EBADR: return DRFLAC_ERROR; + #endif + #ifdef EXFULL + case EXFULL: return DRFLAC_ERROR; + #endif + #ifdef ENOANO + case ENOANO: return DRFLAC_ERROR; + #endif + #ifdef EBADRQC + case EBADRQC: return DRFLAC_ERROR; + #endif + #ifdef EBADSLT + case EBADSLT: return DRFLAC_ERROR; + #endif + #ifdef EBFONT + case EBFONT: return DRFLAC_INVALID_FILE; + #endif + #ifdef ENOSTR + case ENOSTR: return DRFLAC_ERROR; + #endif + #ifdef ENODATA + case ENODATA: return DRFLAC_NO_DATA_AVAILABLE; + #endif + #ifdef ETIME + case ETIME: return DRFLAC_TIMEOUT; + #endif + #ifdef ENOSR + case ENOSR: return DRFLAC_NO_DATA_AVAILABLE; + #endif + #ifdef ENONET + case ENONET: return DRFLAC_NO_NETWORK; + #endif + #ifdef ENOPKG + case ENOPKG: return DRFLAC_ERROR; + #endif + #ifdef EREMOTE + case EREMOTE: return DRFLAC_ERROR; + #endif + #ifdef ENOLINK + case ENOLINK: return DRFLAC_ERROR; + #endif + #ifdef EADV + case EADV: return DRFLAC_ERROR; + #endif + #ifdef ESRMNT + case ESRMNT: return DRFLAC_ERROR; + #endif + #ifdef ECOMM + case ECOMM: return DRFLAC_ERROR; + #endif + #ifdef EPROTO + case EPROTO: return DRFLAC_ERROR; + #endif + #ifdef EMULTIHOP + case EMULTIHOP: return DRFLAC_ERROR; + #endif + #ifdef EDOTDOT + case EDOTDOT: return DRFLAC_ERROR; + #endif + #ifdef EBADMSG + case EBADMSG: return DRFLAC_BAD_MESSAGE; + #endif + #ifdef EOVERFLOW + case EOVERFLOW: return DRFLAC_TOO_BIG; + #endif + #ifdef ENOTUNIQ + case ENOTUNIQ: return DRFLAC_NOT_UNIQUE; + #endif + #ifdef EBADFD + case EBADFD: return DRFLAC_ERROR; + #endif + #ifdef EREMCHG + case EREMCHG: return DRFLAC_ERROR; + #endif + #ifdef ELIBACC + case ELIBACC: return DRFLAC_ACCESS_DENIED; + #endif + #ifdef ELIBBAD + case ELIBBAD: return DRFLAC_INVALID_FILE; + #endif + #ifdef ELIBSCN + case ELIBSCN: return DRFLAC_INVALID_FILE; + #endif + #ifdef ELIBMAX + case ELIBMAX: return DRFLAC_ERROR; + #endif + #ifdef ELIBEXEC + case ELIBEXEC: return DRFLAC_ERROR; + #endif + #ifdef EILSEQ + case EILSEQ: return DRFLAC_INVALID_DATA; + #endif + #ifdef ERESTART + case ERESTART: return DRFLAC_ERROR; + #endif + #ifdef ESTRPIPE + case ESTRPIPE: return DRFLAC_ERROR; + #endif + #ifdef EUSERS + case EUSERS: return DRFLAC_ERROR; + #endif + #ifdef ENOTSOCK + case ENOTSOCK: return DRFLAC_NOT_SOCKET; + #endif + #ifdef EDESTADDRREQ + case EDESTADDRREQ: return DRFLAC_NO_ADDRESS; + #endif + #ifdef EMSGSIZE + case EMSGSIZE: return DRFLAC_TOO_BIG; + #endif + #ifdef EPROTOTYPE + case EPROTOTYPE: return DRFLAC_BAD_PROTOCOL; + #endif + #ifdef ENOPROTOOPT + case ENOPROTOOPT: return DRFLAC_PROTOCOL_UNAVAILABLE; + #endif + #ifdef EPROTONOSUPPORT + case EPROTONOSUPPORT: return DRFLAC_PROTOCOL_NOT_SUPPORTED; + #endif + #ifdef ESOCKTNOSUPPORT + case ESOCKTNOSUPPORT: return DRFLAC_SOCKET_NOT_SUPPORTED; + #endif + #ifdef EOPNOTSUPP + case EOPNOTSUPP: return DRFLAC_INVALID_OPERATION; + #endif + #ifdef EPFNOSUPPORT + case EPFNOSUPPORT: return DRFLAC_PROTOCOL_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EAFNOSUPPORT + case EAFNOSUPPORT: return DRFLAC_ADDRESS_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EADDRINUSE + case EADDRINUSE: return DRFLAC_ALREADY_IN_USE; + #endif + #ifdef EADDRNOTAVAIL + case EADDRNOTAVAIL: return DRFLAC_ERROR; + #endif + #ifdef ENETDOWN + case ENETDOWN: return DRFLAC_NO_NETWORK; + #endif + #ifdef ENETUNREACH + case ENETUNREACH: return DRFLAC_NO_NETWORK; + #endif + #ifdef ENETRESET + case ENETRESET: return DRFLAC_NO_NETWORK; + #endif + #ifdef ECONNABORTED + case ECONNABORTED: return DRFLAC_NO_NETWORK; + #endif + #ifdef ECONNRESET + case ECONNRESET: return DRFLAC_CONNECTION_RESET; + #endif + #ifdef ENOBUFS + case ENOBUFS: return DRFLAC_NO_SPACE; + #endif + #ifdef EISCONN + case EISCONN: return DRFLAC_ALREADY_CONNECTED; + #endif + #ifdef ENOTCONN + case ENOTCONN: return DRFLAC_NOT_CONNECTED; + #endif + #ifdef ESHUTDOWN + case ESHUTDOWN: return DRFLAC_ERROR; + #endif + #ifdef ETOOMANYREFS + case ETOOMANYREFS: return DRFLAC_ERROR; + #endif + #ifdef ETIMEDOUT + case ETIMEDOUT: return DRFLAC_TIMEOUT; + #endif + #ifdef ECONNREFUSED + case ECONNREFUSED: return DRFLAC_CONNECTION_REFUSED; + #endif + #ifdef EHOSTDOWN + case EHOSTDOWN: return DRFLAC_NO_HOST; + #endif + #ifdef EHOSTUNREACH + case EHOSTUNREACH: return DRFLAC_NO_HOST; + #endif + #ifdef EALREADY + case EALREADY: return DRFLAC_IN_PROGRESS; + #endif + #ifdef EINPROGRESS + case EINPROGRESS: return DRFLAC_IN_PROGRESS; + #endif + #ifdef ESTALE + case ESTALE: return DRFLAC_INVALID_FILE; + #endif + #ifdef EUCLEAN + case EUCLEAN: return DRFLAC_ERROR; + #endif + #ifdef ENOTNAM + case ENOTNAM: return DRFLAC_ERROR; + #endif + #ifdef ENAVAIL + case ENAVAIL: return DRFLAC_ERROR; + #endif + #ifdef EISNAM + case EISNAM: return DRFLAC_ERROR; + #endif + #ifdef EREMOTEIO + case EREMOTEIO: return DRFLAC_IO_ERROR; + #endif + #ifdef EDQUOT + case EDQUOT: return DRFLAC_NO_SPACE; + #endif + #ifdef ENOMEDIUM + case ENOMEDIUM: return DRFLAC_DOES_NOT_EXIST; + #endif + #ifdef EMEDIUMTYPE + case EMEDIUMTYPE: return DRFLAC_ERROR; + #endif + #ifdef ECANCELED + case ECANCELED: return DRFLAC_CANCELLED; + #endif + #ifdef ENOKEY + case ENOKEY: return DRFLAC_ERROR; + #endif + #ifdef EKEYEXPIRED + case EKEYEXPIRED: return DRFLAC_ERROR; + #endif + #ifdef EKEYREVOKED + case EKEYREVOKED: return DRFLAC_ERROR; + #endif + #ifdef EKEYREJECTED + case EKEYREJECTED: return DRFLAC_ERROR; + #endif + #ifdef EOWNERDEAD + case EOWNERDEAD: return DRFLAC_ERROR; + #endif + #ifdef ENOTRECOVERABLE + case ENOTRECOVERABLE: return DRFLAC_ERROR; + #endif + #ifdef ERFKILL + case ERFKILL: return DRFLAC_ERROR; + #endif + #ifdef EHWPOISON + case EHWPOISON: return DRFLAC_ERROR; + #endif + default: return DRFLAC_ERROR; + } +} +/* End Errno */ + +/* fopen */ +static drflac_result drflac_fopen(FILE** ppFile, const char* pFilePath, const char* pOpenMode) +{ +#if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err; +#endif + + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRFLAC_INVALID_ARGS; + } + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + err = fopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drflac_result_from_errno(err); + } +#else +#if defined(_WIN32) || defined(__APPLE__) + *ppFile = fopen(pFilePath, pOpenMode); +#else + #if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 && defined(_LARGEFILE64_SOURCE) + *ppFile = fopen64(pFilePath, pOpenMode); + #else + *ppFile = fopen(pFilePath, pOpenMode); + #endif +#endif + if (*ppFile == NULL) { + drflac_result result = drflac_result_from_errno(errno); + if (result == DRFLAC_SUCCESS) { + result = DRFLAC_ERROR; /* Just a safety check to make sure we never ever return success when pFile == NULL. */ + } + + return result; + } +#endif + + return DRFLAC_SUCCESS; +} + +/* +_wfopen() isn't always available in all compilation environments. + + * Windows only. + * MSVC seems to support it universally as far back as VC6 from what I can tell (haven't checked further back). + * MinGW-64 (both 32- and 64-bit) seems to support it. + * MinGW wraps it in !defined(__STRICT_ANSI__). + * OpenWatcom wraps it in !defined(_NO_EXT_KEYS). + +This can be reviewed as compatibility issues arise. The preference is to use _wfopen_s() and _wfopen() as opposed to the wcsrtombs() +fallback, so if you notice your compiler not detecting this properly I'm happy to look at adding support. +*/ +#if defined(_WIN32) + #if defined(_MSC_VER) || defined(__MINGW64__) || (!defined(__STRICT_ANSI__) && !defined(_NO_EXT_KEYS)) + #define DRFLAC_HAS_WFOPEN + #endif +#endif + +#ifndef DR_FLAC_NO_WCHAR +static drflac_result drflac_wfopen(FILE** ppFile, const wchar_t* pFilePath, const wchar_t* pOpenMode, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRFLAC_INVALID_ARGS; + } + +#if defined(DRFLAC_HAS_WFOPEN) + { + /* Use _wfopen() on Windows. */ + #if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err = _wfopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drflac_result_from_errno(err); + } + #else + *ppFile = _wfopen(pFilePath, pOpenMode); + if (*ppFile == NULL) { + return drflac_result_from_errno(errno); + } + #endif + (void)pAllocationCallbacks; + } +#else + /* + Use fopen() on anything other than Windows. Requires a conversion. This is annoying because + fopen() is locale specific. The only real way I can think of to do this is with wcsrtombs(). Note + that wcstombs() is apparently not thread-safe because it uses a static global mbstate_t object for + maintaining state. I've checked this with -std=c89 and it works, but if somebody get's a compiler + error I'll look into improving compatibility. + */ + + /* + Some compilers don't support wchar_t or wcsrtombs() which we're using below. In this case we just + need to abort with an error. If you encounter a compiler lacking such support, add it to this list + and submit a bug report and it'll be added to the library upstream. + */ + #if defined(__DJGPP__) + { + /* Nothing to do here. This will fall through to the error check below. */ + } + #else + { + mbstate_t mbs; + size_t lenMB; + const wchar_t* pFilePathTemp = pFilePath; + char* pFilePathMB = NULL; + char pOpenModeMB[32] = {0}; + + /* Get the length first. */ + DRFLAC_ZERO_OBJECT(&mbs); + lenMB = wcsrtombs(NULL, &pFilePathTemp, 0, &mbs); + if (lenMB == (size_t)-1) { + return drflac_result_from_errno(errno); + } + + pFilePathMB = (char*)drflac__malloc_from_callbacks(lenMB + 1, pAllocationCallbacks); + if (pFilePathMB == NULL) { + return DRFLAC_OUT_OF_MEMORY; + } + + pFilePathTemp = pFilePath; + DRFLAC_ZERO_OBJECT(&mbs); + wcsrtombs(pFilePathMB, &pFilePathTemp, lenMB + 1, &mbs); + + /* The open mode should always consist of ASCII characters so we should be able to do a trivial conversion. */ + { + size_t i = 0; + for (;;) { + if (pOpenMode[i] == 0) { + pOpenModeMB[i] = '\0'; + break; + } + + pOpenModeMB[i] = (char)pOpenMode[i]; + i += 1; + } + } + + *ppFile = fopen(pFilePathMB, pOpenModeMB); + + drflac__free_from_callbacks(pFilePathMB, pAllocationCallbacks); + } + #endif + + if (*ppFile == NULL) { + return DRFLAC_ERROR; + } +#endif + + return DRFLAC_SUCCESS; +} +#endif +/* End fopen */ + +static size_t drflac__on_read_stdio(void* pUserData, void* bufferOut, size_t bytesToRead) +{ + return fread(bufferOut, 1, bytesToRead, (FILE*)pUserData); +} + +static drflac_bool32 drflac__on_seek_stdio(void* pUserData, int offset, drflac_seek_origin origin) +{ + int whence = SEEK_SET; + if (origin == DRFLAC_SEEK_CUR) { + whence = SEEK_CUR; + } else if (origin == DRFLAC_SEEK_END) { + whence = SEEK_END; + } + + return fseek((FILE*)pUserData, offset, whence) == 0; +} + +static drflac_bool32 drflac__on_tell_stdio(void* pUserData, drflac_int64* pCursor) +{ + FILE* pFileStdio = (FILE*)pUserData; + drflac_int64 result; + + /* These were all validated at a higher level. */ + DRFLAC_ASSERT(pFileStdio != NULL); + DRFLAC_ASSERT(pCursor != NULL); + +#if defined(_WIN32) && !defined(NXDK) + #if defined(_MSC_VER) && _MSC_VER > 1200 + result = _ftelli64(pFileStdio); + #else + result = ftell(pFileStdio); + #endif +#else + result = ftell(pFileStdio); +#endif + + *pCursor = result; + + return DRFLAC_TRUE; +} + + + +DRFLAC_API drflac* drflac_open_file(const char* pFileName, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + FILE* pFile; + + if (drflac_fopen(&pFile, pFileName, "rb") != DRFLAC_SUCCESS) { + return NULL; + } + + pFlac = drflac_open(drflac__on_read_stdio, drflac__on_seek_stdio, drflac__on_tell_stdio, (void*)pFile, pAllocationCallbacks); + if (pFlac == NULL) { + fclose(pFile); + return NULL; + } + + return pFlac; +} + +#ifndef DR_FLAC_NO_WCHAR +DRFLAC_API drflac* drflac_open_file_w(const wchar_t* pFileName, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + FILE* pFile; + + if (drflac_wfopen(&pFile, pFileName, L"rb", pAllocationCallbacks) != DRFLAC_SUCCESS) { + return NULL; + } + + pFlac = drflac_open(drflac__on_read_stdio, drflac__on_seek_stdio, drflac__on_tell_stdio, (void*)pFile, pAllocationCallbacks); + if (pFlac == NULL) { + fclose(pFile); + return NULL; + } + + return pFlac; +} +#endif + +DRFLAC_API drflac* drflac_open_file_with_metadata(const char* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + FILE* pFile; + + if (drflac_fopen(&pFile, pFileName, "rb") != DRFLAC_SUCCESS) { + return NULL; + } + + pFlac = drflac_open_with_metadata_private(drflac__on_read_stdio, drflac__on_seek_stdio, drflac__on_tell_stdio, onMeta, drflac_container_unknown, (void*)pFile, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + fclose(pFile); + return pFlac; + } + + return pFlac; +} + +#ifndef DR_FLAC_NO_WCHAR +DRFLAC_API drflac* drflac_open_file_with_metadata_w(const wchar_t* pFileName, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + FILE* pFile; + + if (drflac_wfopen(&pFile, pFileName, L"rb", pAllocationCallbacks) != DRFLAC_SUCCESS) { + return NULL; + } + + pFlac = drflac_open_with_metadata_private(drflac__on_read_stdio, drflac__on_seek_stdio, drflac__on_tell_stdio, onMeta, drflac_container_unknown, (void*)pFile, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + fclose(pFile); + return pFlac; + } + + return pFlac; +} +#endif +#endif /* DR_FLAC_NO_STDIO */ + +static size_t drflac__on_read_memory(void* pUserData, void* bufferOut, size_t bytesToRead) +{ + drflac__memory_stream* memoryStream = (drflac__memory_stream*)pUserData; + size_t bytesRemaining; + + DRFLAC_ASSERT(memoryStream != NULL); + DRFLAC_ASSERT(memoryStream->dataSize >= memoryStream->currentReadPos); + + bytesRemaining = memoryStream->dataSize - memoryStream->currentReadPos; + if (bytesToRead > bytesRemaining) { + bytesToRead = bytesRemaining; + } + + if (bytesToRead > 0) { + DRFLAC_COPY_MEMORY(bufferOut, memoryStream->data + memoryStream->currentReadPos, bytesToRead); + memoryStream->currentReadPos += bytesToRead; + } + + return bytesToRead; +} + +static drflac_bool32 drflac__on_seek_memory(void* pUserData, int offset, drflac_seek_origin origin) +{ + drflac__memory_stream* memoryStream = (drflac__memory_stream*)pUserData; + drflac_int64 newCursor; + + DRFLAC_ASSERT(memoryStream != NULL); + + if (origin == DRFLAC_SEEK_SET) { + newCursor = 0; + } else if (origin == DRFLAC_SEEK_CUR) { + newCursor = (drflac_int64)memoryStream->currentReadPos; + } else if (origin == DRFLAC_SEEK_END) { + newCursor = (drflac_int64)memoryStream->dataSize; + } else { + DRFLAC_ASSERT(!"Invalid seek origin"); + return DRFLAC_FALSE; + } + + newCursor += offset; + + if (newCursor < 0) { + return DRFLAC_FALSE; /* Trying to seek prior to the start of the buffer. */ + } + if ((size_t)newCursor > memoryStream->dataSize) { + return DRFLAC_FALSE; /* Trying to seek beyond the end of the buffer. */ + } + + memoryStream->currentReadPos = (size_t)newCursor; + + return DRFLAC_TRUE; +} + +static drflac_bool32 drflac__on_tell_memory(void* pUserData, drflac_int64* pCursor) +{ + drflac__memory_stream* memoryStream = (drflac__memory_stream*)pUserData; + + DRFLAC_ASSERT(memoryStream != NULL); + DRFLAC_ASSERT(pCursor != NULL); + + *pCursor = (drflac_int64)memoryStream->currentReadPos; + return DRFLAC_TRUE; +} + +DRFLAC_API drflac* drflac_open_memory(const void* pData, size_t dataSize, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac__memory_stream memoryStream; + drflac* pFlac; + + memoryStream.data = (const drflac_uint8*)pData; + memoryStream.dataSize = dataSize; + memoryStream.currentReadPos = 0; + pFlac = drflac_open(drflac__on_read_memory, drflac__on_seek_memory, drflac__on_tell_memory, &memoryStream, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + pFlac->memoryStream = memoryStream; + + /* This is an awful hack... */ +#ifndef DR_FLAC_NO_OGG + if (pFlac->container == drflac_container_ogg) + { + drflac_oggbs* oggbs = (drflac_oggbs*)pFlac->_oggbs; + oggbs->pUserData = &pFlac->memoryStream; + } + else +#endif + { + pFlac->bs.pUserData = &pFlac->memoryStream; + } + + return pFlac; +} + +DRFLAC_API drflac* drflac_open_memory_with_metadata(const void* pData, size_t dataSize, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac__memory_stream memoryStream; + drflac* pFlac; + + memoryStream.data = (const drflac_uint8*)pData; + memoryStream.dataSize = dataSize; + memoryStream.currentReadPos = 0; + pFlac = drflac_open_with_metadata_private(drflac__on_read_memory, drflac__on_seek_memory, drflac__on_tell_memory, onMeta, drflac_container_unknown, &memoryStream, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + pFlac->memoryStream = memoryStream; + + /* This is an awful hack... */ +#ifndef DR_FLAC_NO_OGG + if (pFlac->container == drflac_container_ogg) + { + drflac_oggbs* oggbs = (drflac_oggbs*)pFlac->_oggbs; + oggbs->pUserData = &pFlac->memoryStream; + } + else +#endif + { + pFlac->bs.pUserData = &pFlac->memoryStream; + } + + return pFlac; +} + + + +DRFLAC_API drflac* drflac_open(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + return drflac_open_with_metadata_private(onRead, onSeek, onTell, NULL, drflac_container_unknown, pUserData, pUserData, pAllocationCallbacks); +} +DRFLAC_API drflac* drflac_open_relaxed(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_container container, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + return drflac_open_with_metadata_private(onRead, onSeek, onTell, NULL, container, pUserData, pUserData, pAllocationCallbacks); +} + +DRFLAC_API drflac* drflac_open_with_metadata(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + return drflac_open_with_metadata_private(onRead, onSeek, onTell, onMeta, drflac_container_unknown, pUserData, pUserData, pAllocationCallbacks); +} +DRFLAC_API drflac* drflac_open_with_metadata_relaxed(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, drflac_meta_proc onMeta, drflac_container container, void* pUserData, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + return drflac_open_with_metadata_private(onRead, onSeek, onTell, onMeta, container, pUserData, pUserData, pAllocationCallbacks); +} + +DRFLAC_API void drflac_close(drflac* pFlac) { if (pFlac == NULL) { return; } #ifndef DR_FLAC_NO_STDIO - // If we opened the file with drflac_open_file() we will want to close the file handle. We can know whether or not drflac_open_file() - // was used by looking at the callbacks. - if (pFlac->onRead == drflac__on_read_stdio) { -#if defined(DR_OPUS_NO_WIN32_IO) || !defined(_WIN32) - fclose((FILE*)pFlac->pUserData); + /* + If we opened the file with drflac_open_file() we will want to close the file handle. We can know whether or not drflac_open_file() + was used by looking at the callbacks. + */ + if (pFlac->bs.onRead == drflac__on_read_stdio) { + fclose((FILE*)pFlac->bs.pUserData); + } + +#ifndef DR_FLAC_NO_OGG + /* Need to clean up Ogg streams a bit differently due to the way the bit streaming is chained. */ + if (pFlac->container == drflac_container_ogg) { + drflac_oggbs* oggbs = (drflac_oggbs*)pFlac->_oggbs; + DRFLAC_ASSERT(pFlac->bs.onRead == drflac__on_read_ogg); + + if (oggbs->onRead == drflac__on_read_stdio) { + fclose((FILE*)oggbs->pUserData); + } + } +#endif +#endif + + drflac__free_from_callbacks(pFlac, &pFlac->allocationCallbacks); +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 left = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 left0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 left1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 left2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 left3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 right0 = left0 - side0; + drflac_uint32 right1 = left1 - side1; + drflac_uint32 right2 = left2 - side2; + drflac_uint32 right3 = left3 - side3; + + pOutputSamples[i*8+0] = (drflac_int32)left0; + pOutputSamples[i*8+1] = (drflac_int32)right0; + pOutputSamples[i*8+2] = (drflac_int32)left1; + pOutputSamples[i*8+3] = (drflac_int32)right1; + pOutputSamples[i*8+4] = (drflac_int32)left2; + pOutputSamples[i*8+5] = (drflac_int32)right2; + pOutputSamples[i*8+6] = (drflac_int32)left3; + pOutputSamples[i*8+7] = (drflac_int32)right3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i right = _mm_sub_epi32(left, side); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t left; + uint32x4_t side; + uint32x4_t right; + + left = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + right = vsubq_u32(left, side); + + drflac__vst2q_u32((drflac_uint32*)pOutputSamples + i*8, vzipq_u32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_left_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_left_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_left_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s32__decode_left_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); #else - CloseHandle((HANDLE)pFlac->pUserData); + drflac_read_pcm_frames_s32__decode_left_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); #endif } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 side = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 right = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} #endif - // If we opened the file with drflac_open_memory() we will want to free() the user data. - if (pFlac->onRead == drflac__on_read_memory) { - free(pFlac->pUserData); +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 side0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 side1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 side2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 side3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 right0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 right1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 right2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 right3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 left0 = right0 + side0; + drflac_uint32 left1 = right1 + side1; + drflac_uint32 left2 = right2 + side2; + drflac_uint32 left3 = right3 + side3; + + pOutputSamples[i*8+0] = (drflac_int32)left0; + pOutputSamples[i*8+1] = (drflac_int32)right0; + pOutputSamples[i*8+2] = (drflac_int32)left1; + pOutputSamples[i*8+3] = (drflac_int32)right1; + pOutputSamples[i*8+4] = (drflac_int32)left2; + pOutputSamples[i*8+5] = (drflac_int32)right2; + pOutputSamples[i*8+6] = (drflac_int32)left3; + pOutputSamples[i*8+7] = (drflac_int32)right3; } - free(pFlac); + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } } -static uint64_t drflac__read_s16__misaligned(drflac* pFlac, uint64_t samplesToRead, int16_t* bufferOut) +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) { - unsigned int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFrame.channelAssignment); + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; - // We should never be calling this when the number of samples to read is >= the sample count. - assert(samplesToRead < channelCount); - assert(pFlac->currentFrame.samplesRemaining > 0 && samplesToRead <= pFlac->currentFrame.samplesRemaining); + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + for (i = 0; i < frameCount4; ++i) { + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i left = _mm_add_epi32(right, side); - uint64_t samplesRead = 0; - while (samplesToRead > 0) + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t side; + uint32x4_t right; + uint32x4_t left; + + side = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + right = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + left = vaddq_u32(right, side); + + drflac__vst2q_u32((drflac_uint32*)pOutputSamples + i*8, vzipq_u32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left; + pOutputSamples[i*2+1] = (drflac_int32)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_right_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_right_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_right_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif { - uint64_t totalSamplesInFrame = pFlac->currentFrame.blockSize * channelCount; - uint64_t samplesReadFromFrameSoFar = totalSamplesInFrame - pFlac->currentFrame.samplesRemaining; - unsigned int channelIndex = samplesReadFromFrameSoFar % channelCount; - - unsigned long long nextSampleInFrame = samplesReadFromFrameSoFar / channelCount; - - int decodedSample = 0; - switch (pFlac->currentFrame.channelAssignment) - { - case DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE: - { - if (channelIndex == 0) { - decodedSample = pFlac->currentFrame.subframes[channelIndex].pDecodedSamples[nextSampleInFrame]; - } else { - int side = pFlac->currentFrame.subframes[channelIndex + 0].pDecodedSamples[nextSampleInFrame]; - int left = pFlac->currentFrame.subframes[channelIndex - 1].pDecodedSamples[nextSampleInFrame]; - decodedSample = left - side; - } - - } break; - - case DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE: - { - if (channelIndex == 0) { - int side = pFlac->currentFrame.subframes[channelIndex + 0].pDecodedSamples[nextSampleInFrame]; - int right = pFlac->currentFrame.subframes[channelIndex + 1].pDecodedSamples[nextSampleInFrame]; - decodedSample = side + right; - } else { - decodedSample = pFlac->currentFrame.subframes[channelIndex].pDecodedSamples[nextSampleInFrame]; - } - - } break; - - case DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE: - { - int mid; - int side; - if (channelIndex == 0) { - mid = pFlac->currentFrame.subframes[channelIndex + 0].pDecodedSamples[nextSampleInFrame]; - side = pFlac->currentFrame.subframes[channelIndex + 1].pDecodedSamples[nextSampleInFrame]; - - mid = (((unsigned int)mid) << 1) | (side & 0x01); - decodedSample = (mid + side) >> 1; - } else { - mid = pFlac->currentFrame.subframes[channelIndex - 1].pDecodedSamples[nextSampleInFrame]; - side = pFlac->currentFrame.subframes[channelIndex + 0].pDecodedSamples[nextSampleInFrame]; - - mid = (((unsigned int)mid) << 1) | (side & 0x01); - decodedSample = (mid - side) >> 1; - } - - } break; - - case DRFLAC_CHANNEL_ASSIGNMENT_INDEPENDENT: - default: - { - decodedSample = pFlac->currentFrame.subframes[channelIndex].pDecodedSamples[nextSampleInFrame]; - } break; - } - - int shift = (16 - pFlac->bitsPerSample) + pFlac->currentFrame.subframes[channelIndex].wastedBitsPerSample; - if (shift >= 0) { - decodedSample <<= shift; - } else { - decodedSample >>= -shift; - } - - if (bufferOut) { - *bufferOut++ = decodedSample; - } - - samplesRead += 1; - pFlac->currentFrame.samplesRemaining -= 1; - samplesToRead -= 1; + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s32__decode_right_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s32__decode_right_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif } - - return samplesRead; } -static uint64_t drflac__seek_forward_by_samples(drflac* pFlac, uint64_t samplesToRead) + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) { - uint64_t samplesRead = 0; - while (samplesToRead > 0) + for (drflac_uint64 i = 0; i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample); + pOutputSamples[i*2+1] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_int32 shift = unusedBitsPerSample; + + if (shift > 0) { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (mid0 + side0) << shift; + temp1L = (mid1 + side1) << shift; + temp2L = (mid2 + side2) << shift; + temp3L = (mid3 + side3) << shift; + + temp0R = (mid0 - side0) << shift; + temp1R = (mid1 - side1) << shift; + temp2R = (mid2 - side2) << shift; + temp3R = (mid3 - side3) << shift; + + pOutputSamples[i*8+0] = (drflac_int32)temp0L; + pOutputSamples[i*8+1] = (drflac_int32)temp0R; + pOutputSamples[i*8+2] = (drflac_int32)temp1L; + pOutputSamples[i*8+3] = (drflac_int32)temp1R; + pOutputSamples[i*8+4] = (drflac_int32)temp2L; + pOutputSamples[i*8+5] = (drflac_int32)temp2R; + pOutputSamples[i*8+6] = (drflac_int32)temp3L; + pOutputSamples[i*8+7] = (drflac_int32)temp3R; + } + } else { + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (drflac_uint32)((drflac_int32)(mid0 + side0) >> 1); + temp1L = (drflac_uint32)((drflac_int32)(mid1 + side1) >> 1); + temp2L = (drflac_uint32)((drflac_int32)(mid2 + side2) >> 1); + temp3L = (drflac_uint32)((drflac_int32)(mid3 + side3) >> 1); + + temp0R = (drflac_uint32)((drflac_int32)(mid0 - side0) >> 1); + temp1R = (drflac_uint32)((drflac_int32)(mid1 - side1) >> 1); + temp2R = (drflac_uint32)((drflac_int32)(mid2 - side2) >> 1); + temp3R = (drflac_uint32)((drflac_int32)(mid3 - side3) >> 1); + + pOutputSamples[i*8+0] = (drflac_int32)temp0L; + pOutputSamples[i*8+1] = (drflac_int32)temp0R; + pOutputSamples[i*8+2] = (drflac_int32)temp1L; + pOutputSamples[i*8+3] = (drflac_int32)temp1R; + pOutputSamples[i*8+4] = (drflac_int32)temp2L; + pOutputSamples[i*8+5] = (drflac_int32)temp2R; + pOutputSamples[i*8+6] = (drflac_int32)temp3L; + pOutputSamples[i*8+7] = (drflac_int32)temp3R; + } + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample); + pOutputSamples[i*2+1] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample); + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_int32 shift = unusedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i left; + __m128i right; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + left = _mm_srai_epi32(_mm_add_epi32(mid, side), 1); + right = _mm_srai_epi32(_mm_sub_epi32(mid, side), 1); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)(mid + side) >> 1; + pOutputSamples[i*2+1] = (drflac_int32)(mid - side) >> 1; + } + } else { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i left; + __m128i right; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + left = _mm_slli_epi32(_mm_add_epi32(mid, side), shift); + right = _mm_slli_epi32(_mm_sub_epi32(mid, side), shift); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((mid + side) << shift); + pOutputSamples[i*2+1] = (drflac_int32)((mid - side) << shift); + } + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_int32 shift = unusedBitsPerSample; + int32x4_t wbpsShift0_4; /* wbps = Wasted Bits Per Sample */ + int32x4_t wbpsShift1_4; /* wbps = Wasted Bits Per Sample */ + uint32x4_t one4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + wbpsShift0_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + wbpsShift1_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + one4 = vdupq_n_u32(1); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t left; + int32x4_t right; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbpsShift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbpsShift1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, one4)); + + left = vshrq_n_s32(vreinterpretq_s32_u32(vaddq_u32(mid, side)), 1); + right = vshrq_n_s32(vreinterpretq_s32_u32(vsubq_u32(mid, side)), 1); + + drflac__vst2q_s32(pOutputSamples + i*8, vzipq_s32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)(mid + side) >> 1; + pOutputSamples[i*2+1] = (drflac_int32)(mid - side) >> 1; + } + } else { + int32x4_t shift4; + + shift -= 1; + shift4 = vdupq_n_s32(shift); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t left; + int32x4_t right; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbpsShift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbpsShift1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, one4)); + + left = vreinterpretq_s32_u32(vshlq_u32(vaddq_u32(mid, side), shift4)); + right = vreinterpretq_s32_u32(vshlq_u32(vsubq_u32(mid, side), shift4)); + + drflac__vst2q_s32(pOutputSamples + i*8, vzipq_s32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((mid + side) << shift); + pOutputSamples[i*2+1] = (drflac_int32)((mid - side) << shift); + } + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_mid_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_mid_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_mid_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif { - if (pFlac->currentFrame.samplesRemaining == 0) - { - if (!drflac__read_and_decode_next_frame(pFlac)) { - break; // Couldn't read the next frame, so just break from the loop and return. - } - } - else - { - samplesRead += 1; - pFlac->currentFrame.samplesRemaining -= 1; - samplesToRead -= 1; - } + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s32__decode_mid_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s32__decode_mid_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif } - - return samplesRead; } -static uint64_t drflac_read_s16(drflac* pFlac, uint64_t samplesToRead, int16_t* bufferOut) + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) { - // Note that is allowed to be null, in which case this will be treated as something like a seek. - if (pFlac == NULL || samplesToRead == 0) { + for (drflac_uint64 i = 0; i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)((drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample)); + pOutputSamples[i*2+1] = (drflac_int32)((drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample)); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 tempL0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 tempL1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 tempL2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 tempL3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 tempR0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 tempR1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 tempR2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 tempR3 = pInputSamples1U32[i*4+3] << shift1; + + pOutputSamples[i*8+0] = (drflac_int32)tempL0; + pOutputSamples[i*8+1] = (drflac_int32)tempR0; + pOutputSamples[i*8+2] = (drflac_int32)tempL1; + pOutputSamples[i*8+3] = (drflac_int32)tempR1; + pOutputSamples[i*8+4] = (drflac_int32)tempL2; + pOutputSamples[i*8+5] = (drflac_int32)tempR2; + pOutputSamples[i*8+6] = (drflac_int32)tempL3; + pOutputSamples[i*8+7] = (drflac_int32)tempR3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0); + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1); + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 0), _mm_unpacklo_epi32(left, right)); + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8 + 4), _mm_unpackhi_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0); + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1); + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + int32x4_t shift4_0 = vdupq_n_s32(shift0); + int32x4_t shift4_1 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + int32x4_t left; + int32x4_t right; + + left = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift4_0)); + right = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift4_1)); + + drflac__vst2q_s32(pOutputSamples + i*8, vzipq_s32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0); + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s32__decode_independent_stereo(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int32* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_independent_stereo__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s32__decode_independent_stereo__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s32__decode_independent_stereo__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s32__decode_independent_stereo__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_s32(drflac* pFlac, drflac_uint64 framesToRead, drflac_int32* pBufferOut) +{ + drflac_uint64 framesRead; + drflac_uint32 unusedBitsPerSample; + + if (pFlac == NULL || framesToRead == 0) { return 0; } - if (bufferOut == NULL) { - return drflac__seek_forward_by_samples(pFlac, samplesToRead); + if (pBufferOut == NULL) { + return drflac__seek_forward_by_pcm_frames(pFlac, framesToRead); } + DRFLAC_ASSERT(pFlac->bitsPerSample <= 32); + unusedBitsPerSample = 32 - pFlac->bitsPerSample; - uint64_t samplesRead = 0; - while (samplesToRead > 0) - { - // If we've run out of samples in this frame, go to the next. - if (pFlac->currentFrame.samplesRemaining == 0) - { - if (!drflac__read_and_decode_next_frame(pFlac)) { - break; // Couldn't read the next frame, so just break from the loop and return. + framesRead = 0; + while (framesToRead > 0) { + /* If we've run out of samples in this frame, go to the next. */ + if (pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + break; /* Couldn't read the next frame, so just break from the loop and return. */ } - } - else - { - // Here is where we grab the samples and interleave them. + } else { + unsigned int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + drflac_uint64 iFirstPCMFrame = pFlac->currentFLACFrame.header.blockSizeInPCMFrames - pFlac->currentFLACFrame.pcmFramesRemaining; + drflac_uint64 frameCountThisIteration = framesToRead; - unsigned int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFrame.channelAssignment); - uint64_t totalSamplesInFrame = pFlac->currentFrame.blockSize * channelCount; - uint64_t samplesReadFromFrameSoFar = totalSamplesInFrame - pFlac->currentFrame.samplesRemaining; - - int misalignedSampleCount = samplesReadFromFrameSoFar % channelCount; - if (misalignedSampleCount > 0) { - uint64_t misalignedSamplesRead = drflac__read_s16__misaligned(pFlac, misalignedSampleCount, bufferOut); - samplesRead += misalignedSamplesRead; - samplesReadFromFrameSoFar += misalignedSamplesRead; - bufferOut += misalignedSamplesRead; - samplesToRead -= misalignedSamplesRead; + if (frameCountThisIteration > pFlac->currentFLACFrame.pcmFramesRemaining) { + frameCountThisIteration = pFlac->currentFLACFrame.pcmFramesRemaining; } + if (channelCount == 2) { + const drflac_int32* pDecodedSamples0 = pFlac->currentFLACFrame.subframes[0].pSamplesS32 + iFirstPCMFrame; + const drflac_int32* pDecodedSamples1 = pFlac->currentFLACFrame.subframes[1].pSamplesS32 + iFirstPCMFrame; - uint64_t alignedSampleCountPerChannel = samplesToRead / channelCount; - if (alignedSampleCountPerChannel > pFlac->currentFrame.samplesRemaining / channelCount) { - alignedSampleCountPerChannel = pFlac->currentFrame.samplesRemaining / channelCount; - } - - uint64_t firstAlignedSampleInFrame = samplesReadFromFrameSoFar / channelCount; - int unusedBitsPerSample = 16 - pFlac->bitsPerSample; - - if (unusedBitsPerSample >= 0) { - int lshift0 = unusedBitsPerSample + pFlac->currentFrame.subframes[0].wastedBitsPerSample; - int lshift1 = unusedBitsPerSample + pFlac->currentFrame.subframes[1].wastedBitsPerSample; - - switch (pFlac->currentFrame.channelAssignment) + switch (pFlac->currentFLACFrame.header.channelAssignment) { case DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE: { - const int* pDecodedSamples0 = pFlac->currentFrame.subframes[0].pDecodedSamples + firstAlignedSampleInFrame; - const int* pDecodedSamples1 = pFlac->currentFrame.subframes[1].pDecodedSamples + firstAlignedSampleInFrame; - - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - int left = pDecodedSamples0[i]; - int side = pDecodedSamples1[i]; - int right = left - side; - - bufferOut[i*2+0] = left << lshift0; - bufferOut[i*2+1] = right << lshift1; - } + drflac_read_pcm_frames_s32__decode_left_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); } break; case DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE: { - const int* pDecodedSamples0 = pFlac->currentFrame.subframes[0].pDecodedSamples + firstAlignedSampleInFrame; - const int* pDecodedSamples1 = pFlac->currentFrame.subframes[1].pDecodedSamples + firstAlignedSampleInFrame; - - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - int side = pDecodedSamples0[i]; - int right = pDecodedSamples1[i]; - int left = right + side; - - bufferOut[i*2+0] = left << lshift0; - bufferOut[i*2+1] = right << lshift1; - } + drflac_read_pcm_frames_s32__decode_right_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); } break; case DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE: { - const int* pDecodedSamples0 = pFlac->currentFrame.subframes[0].pDecodedSamples + firstAlignedSampleInFrame; - const int* pDecodedSamples1 = pFlac->currentFrame.subframes[1].pDecodedSamples + firstAlignedSampleInFrame; - - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - int side = pDecodedSamples1[i]; - int mid = (((uint32_t)pDecodedSamples0[i]) << 1) | (side & 0x01); - - bufferOut[i*2+0] = ((mid + side) >> 1) << lshift0; - bufferOut[i*2+1] = ((mid - side) >> 1) << lshift1; - } + drflac_read_pcm_frames_s32__decode_mid_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); } break; case DRFLAC_CHANNEL_ASSIGNMENT_INDEPENDENT: default: { - if (pFlac->currentFrame.channelAssignment == 1) // 1 = Stereo - { - // Stereo optimized inner loop unroll. - const int* pDecodedSamples0 = pFlac->currentFrame.subframes[0].pDecodedSamples + firstAlignedSampleInFrame; - const int* pDecodedSamples1 = pFlac->currentFrame.subframes[1].pDecodedSamples + firstAlignedSampleInFrame; - - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - bufferOut[i*2+0] = pDecodedSamples0[i] << lshift0; - bufferOut[i*2+1] = pDecodedSamples1[i] << lshift1; - } - } - else - { - // Generic interleaving. - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - for (unsigned int j = 0; j < channelCount; ++j) { - bufferOut[(i*channelCount)+j] = (pFlac->currentFrame.subframes[j].pDecodedSamples[firstAlignedSampleInFrame + i]) << (unusedBitsPerSample + pFlac->currentFrame.subframes[j].wastedBitsPerSample); - } - } - } + drflac_read_pcm_frames_s32__decode_independent_stereo(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); } break; } } else { - int rshift0 = -unusedBitsPerSample + pFlac->currentFrame.subframes[0].wastedBitsPerSample; - int rshift1 = -unusedBitsPerSample + pFlac->currentFrame.subframes[1].wastedBitsPerSample; + /* Generic interleaving. */ + drflac_uint64 i; + for (i = 0; i < frameCountThisIteration; ++i) { + unsigned int j; + for (j = 0; j < channelCount; ++j) { + pBufferOut[(i*channelCount)+j] = (drflac_int32)((drflac_uint32)(pFlac->currentFLACFrame.subframes[j].pSamplesS32[iFirstPCMFrame + i]) << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[j].wastedBitsPerSample)); + } + } + } - switch (pFlac->currentFrame.channelAssignment) + framesRead += frameCountThisIteration; + pBufferOut += frameCountThisIteration * channelCount; + framesToRead -= frameCountThisIteration; + pFlac->currentPCMFrame += frameCountThisIteration; + pFlac->currentFLACFrame.pcmFramesRemaining -= (drflac_uint32)frameCountThisIteration; + } + } + + return framesRead; +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 left = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 right = left - side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 left0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 left1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 left2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 left3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 right0 = left0 - side0; + drflac_uint32 right1 = left1 - side1; + drflac_uint32 right2 = left2 - side2; + drflac_uint32 right3 = left3 - side3; + + left0 >>= 16; + left1 >>= 16; + left2 >>= 16; + left3 >>= 16; + + right0 >>= 16; + right1 >>= 16; + right2 >>= 16; + right3 >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)left0; + pOutputSamples[i*8+1] = (drflac_int16)right0; + pOutputSamples[i*8+2] = (drflac_int16)left1; + pOutputSamples[i*8+3] = (drflac_int16)right1; + pOutputSamples[i*8+4] = (drflac_int16)left2; + pOutputSamples[i*8+5] = (drflac_int16)right2; + pOutputSamples[i*8+6] = (drflac_int16)left3; + pOutputSamples[i*8+7] = (drflac_int16)right3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i right = _mm_sub_epi32(left, side); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t left; + uint32x4_t side; + uint32x4_t right; + + left = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + right = vsubq_u32(left, side); + + left = vshrq_n_u32(left, 16); + right = vshrq_n_u32(right, 16); + + drflac__vst2q_u16((drflac_uint16*)pOutputSamples + i*8, vzip_u16(vmovn_u32(left), vmovn_u32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_left_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_left_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_left_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s16__decode_left_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s16__decode_left_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 side = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 right = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 left = right + side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 side0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 side1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 side2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 side3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 right0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 right1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 right2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 right3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 left0 = right0 + side0; + drflac_uint32 left1 = right1 + side1; + drflac_uint32 left2 = right2 + side2; + drflac_uint32 left3 = right3 + side3; + + left0 >>= 16; + left1 >>= 16; + left2 >>= 16; + left3 >>= 16; + + right0 >>= 16; + right1 >>= 16; + right2 >>= 16; + right3 >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)left0; + pOutputSamples[i*8+1] = (drflac_int16)right0; + pOutputSamples[i*8+2] = (drflac_int16)left1; + pOutputSamples[i*8+3] = (drflac_int16)right1; + pOutputSamples[i*8+4] = (drflac_int16)left2; + pOutputSamples[i*8+5] = (drflac_int16)right2; + pOutputSamples[i*8+6] = (drflac_int16)left3; + pOutputSamples[i*8+7] = (drflac_int16)right3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + for (i = 0; i < frameCount4; ++i) { + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i left = _mm_add_epi32(right, side); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t side; + uint32x4_t right; + uint32x4_t left; + + side = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + right = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + left = vaddq_u32(right, side); + + left = vshrq_n_u32(left, 16); + right = vshrq_n_u32(right, 16); + + drflac__vst2q_u16((drflac_uint16*)pOutputSamples + i*8, vzip_u16(vmovn_u32(left), vmovn_u32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + left >>= 16; + right >>= 16; + + pOutputSamples[i*2+0] = (drflac_int16)left; + pOutputSamples[i*2+1] = (drflac_int16)right; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_right_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_right_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_right_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s16__decode_right_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s16__decode_right_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + drflac_uint32 mid = (drflac_uint32)pInputSamples0[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample) >> 16); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample; + + if (shift > 0) { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (mid0 + side0) << shift; + temp1L = (mid1 + side1) << shift; + temp2L = (mid2 + side2) << shift; + temp3L = (mid3 + side3) << shift; + + temp0R = (mid0 - side0) << shift; + temp1R = (mid1 - side1) << shift; + temp2R = (mid2 - side2) << shift; + temp3R = (mid3 - side3) << shift; + + temp0L >>= 16; + temp1L >>= 16; + temp2L >>= 16; + temp3L >>= 16; + + temp0R >>= 16; + temp1R >>= 16; + temp2R >>= 16; + temp3R >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)temp0L; + pOutputSamples[i*8+1] = (drflac_int16)temp0R; + pOutputSamples[i*8+2] = (drflac_int16)temp1L; + pOutputSamples[i*8+3] = (drflac_int16)temp1R; + pOutputSamples[i*8+4] = (drflac_int16)temp2L; + pOutputSamples[i*8+5] = (drflac_int16)temp2R; + pOutputSamples[i*8+6] = (drflac_int16)temp3L; + pOutputSamples[i*8+7] = (drflac_int16)temp3R; + } + } else { + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = ((drflac_int32)(mid0 + side0) >> 1); + temp1L = ((drflac_int32)(mid1 + side1) >> 1); + temp2L = ((drflac_int32)(mid2 + side2) >> 1); + temp3L = ((drflac_int32)(mid3 + side3) >> 1); + + temp0R = ((drflac_int32)(mid0 - side0) >> 1); + temp1R = ((drflac_int32)(mid1 - side1) >> 1); + temp2R = ((drflac_int32)(mid2 - side2) >> 1); + temp3R = ((drflac_int32)(mid3 - side3) >> 1); + + temp0L >>= 16; + temp1L >>= 16; + temp2L >>= 16; + temp3L >>= 16; + + temp0R >>= 16; + temp1R >>= 16; + temp2R >>= 16; + temp3R >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)temp0L; + pOutputSamples[i*8+1] = (drflac_int16)temp0R; + pOutputSamples[i*8+2] = (drflac_int16)temp1L; + pOutputSamples[i*8+3] = (drflac_int16)temp1R; + pOutputSamples[i*8+4] = (drflac_int16)temp2L; + pOutputSamples[i*8+5] = (drflac_int16)temp2R; + pOutputSamples[i*8+6] = (drflac_int16)temp3L; + pOutputSamples[i*8+7] = (drflac_int16)temp3R; + } + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample) >> 16); + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i left; + __m128i right; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + left = _mm_srai_epi32(_mm_add_epi32(mid, side), 1); + right = _mm_srai_epi32(_mm_sub_epi32(mid, side), 1); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((drflac_int32)(mid + side) >> 1) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((drflac_int32)(mid - side) >> 1) >> 16); + } + } else { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i left; + __m128i right; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + left = _mm_slli_epi32(_mm_add_epi32(mid, side), shift); + right = _mm_slli_epi32(_mm_sub_epi32(mid, side), shift); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((mid + side) << shift) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((mid - side) << shift) >> 16); + } + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample; + int32x4_t wbpsShift0_4; /* wbps = Wasted Bits Per Sample */ + int32x4_t wbpsShift1_4; /* wbps = Wasted Bits Per Sample */ + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + wbpsShift0_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + wbpsShift1_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t left; + int32x4_t right; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbpsShift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbpsShift1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, vdupq_n_u32(1))); + + left = vshrq_n_s32(vreinterpretq_s32_u32(vaddq_u32(mid, side)), 1); + right = vshrq_n_s32(vreinterpretq_s32_u32(vsubq_u32(mid, side)), 1); + + left = vshrq_n_s32(left, 16); + right = vshrq_n_s32(right, 16); + + drflac__vst2q_s16(pOutputSamples + i*8, vzip_s16(vmovn_s32(left), vmovn_s32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((drflac_int32)(mid + side) >> 1) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((drflac_int32)(mid - side) >> 1) >> 16); + } + } else { + int32x4_t shift4; + + shift -= 1; + shift4 = vdupq_n_s32(shift); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t left; + int32x4_t right; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbpsShift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbpsShift1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, vdupq_n_u32(1))); + + left = vreinterpretq_s32_u32(vshlq_u32(vaddq_u32(mid, side), shift4)); + right = vreinterpretq_s32_u32(vshlq_u32(vsubq_u32(mid, side), shift4)); + + left = vshrq_n_s32(left, 16); + right = vshrq_n_s32(right, 16); + + drflac__vst2q_s16(pOutputSamples + i*8, vzip_s16(vmovn_s32(left), vmovn_s32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int16)(((mid + side) << shift) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)(((mid - side) << shift) >> 16); + } + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_mid_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_mid_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_mid_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s16__decode_mid_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s16__decode_mid_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int16)((drflac_int32)((drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample)) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)((drflac_int32)((drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample)) >> 16); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 tempL0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 tempL1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 tempL2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 tempL3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 tempR0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 tempR1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 tempR2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 tempR3 = pInputSamples1U32[i*4+3] << shift1; + + tempL0 >>= 16; + tempL1 >>= 16; + tempL2 >>= 16; + tempL3 >>= 16; + + tempR0 >>= 16; + tempR1 >>= 16; + tempR2 >>= 16; + tempR3 >>= 16; + + pOutputSamples[i*8+0] = (drflac_int16)tempL0; + pOutputSamples[i*8+1] = (drflac_int16)tempR0; + pOutputSamples[i*8+2] = (drflac_int16)tempL1; + pOutputSamples[i*8+3] = (drflac_int16)tempR1; + pOutputSamples[i*8+4] = (drflac_int16)tempL2; + pOutputSamples[i*8+5] = (drflac_int16)tempR2; + pOutputSamples[i*8+6] = (drflac_int16)tempL3; + pOutputSamples[i*8+7] = (drflac_int16)tempR3; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int16)((pInputSamples0U32[i] << shift0) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)((pInputSamples1U32[i] << shift1) >> 16); + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + + left = _mm_srai_epi32(left, 16); + right = _mm_srai_epi32(right, 16); + + /* At this point we have results. We can now pack and interleave these into a single __m128i object and then store the in the output buffer. */ + _mm_storeu_si128((__m128i*)(pOutputSamples + i*8), drflac__mm_packs_interleaved_epi32(left, right)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int16)((pInputSamples0U32[i] << shift0) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)((pInputSamples1U32[i] << shift1) >> 16); + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + int32x4_t shift0_4 = vdupq_n_s32(shift0); + int32x4_t shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + int32x4_t left; + int32x4_t right; + + left = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4)); + right = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4)); + + left = vshrq_n_s32(left, 16); + right = vshrq_n_s32(right, 16); + + drflac__vst2q_s16(pOutputSamples + i*8, vzip_s16(vmovn_s32(left), vmovn_s32(right))); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int16)((pInputSamples0U32[i] << shift0) >> 16); + pOutputSamples[i*2+1] = (drflac_int16)((pInputSamples1U32[i] << shift1) >> 16); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_s16__decode_independent_stereo(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, drflac_int16* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_independent_stereo__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_s16__decode_independent_stereo__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_s16__decode_independent_stereo__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_s16__decode_independent_stereo__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_s16(drflac* pFlac, drflac_uint64 framesToRead, drflac_int16* pBufferOut) +{ + drflac_uint64 framesRead; + drflac_uint32 unusedBitsPerSample; + + if (pFlac == NULL || framesToRead == 0) { + return 0; + } + + if (pBufferOut == NULL) { + return drflac__seek_forward_by_pcm_frames(pFlac, framesToRead); + } + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 32); + unusedBitsPerSample = 32 - pFlac->bitsPerSample; + + framesRead = 0; + while (framesToRead > 0) { + /* If we've run out of samples in this frame, go to the next. */ + if (pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + break; /* Couldn't read the next frame, so just break from the loop and return. */ + } + } else { + unsigned int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + drflac_uint64 iFirstPCMFrame = pFlac->currentFLACFrame.header.blockSizeInPCMFrames - pFlac->currentFLACFrame.pcmFramesRemaining; + drflac_uint64 frameCountThisIteration = framesToRead; + + if (frameCountThisIteration > pFlac->currentFLACFrame.pcmFramesRemaining) { + frameCountThisIteration = pFlac->currentFLACFrame.pcmFramesRemaining; + } + + if (channelCount == 2) { + const drflac_int32* pDecodedSamples0 = pFlac->currentFLACFrame.subframes[0].pSamplesS32 + iFirstPCMFrame; + const drflac_int32* pDecodedSamples1 = pFlac->currentFLACFrame.subframes[1].pSamplesS32 + iFirstPCMFrame; + + switch (pFlac->currentFLACFrame.header.channelAssignment) { case DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE: { - const int* pDecodedSamples0 = pFlac->currentFrame.subframes[0].pDecodedSamples + firstAlignedSampleInFrame; - const int* pDecodedSamples1 = pFlac->currentFrame.subframes[1].pDecodedSamples + firstAlignedSampleInFrame; - - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - int left = pDecodedSamples0[i]; - int side = pDecodedSamples1[i]; - int right = left - side; - - bufferOut[i*2+0] = left >> rshift0; - bufferOut[i*2+1] = right >> rshift1; - } + drflac_read_pcm_frames_s16__decode_left_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); } break; case DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE: { - const int* pDecodedSamples0 = pFlac->currentFrame.subframes[0].pDecodedSamples + firstAlignedSampleInFrame; - const int* pDecodedSamples1 = pFlac->currentFrame.subframes[1].pDecodedSamples + firstAlignedSampleInFrame; - - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - int side = pDecodedSamples0[i]; - int right = pDecodedSamples1[i]; - int left = right + side; - - bufferOut[i*2+0] = left >> rshift0; - bufferOut[i*2+1] = right >> rshift1; - } + drflac_read_pcm_frames_s16__decode_right_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); } break; case DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE: { - const int* pDecodedSamples0 = pFlac->currentFrame.subframes[0].pDecodedSamples + firstAlignedSampleInFrame; - const int* pDecodedSamples1 = pFlac->currentFrame.subframes[1].pDecodedSamples + firstAlignedSampleInFrame; - - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - int side = pDecodedSamples1[i]; - int mid = (((uint32_t)pDecodedSamples0[i]) << 1) | (side & 0x01); - - bufferOut[i*2+0] = ((mid + side) >> 1) >> rshift0; - bufferOut[i*2+1] = ((mid - side) >> 1) >> rshift1; - } + drflac_read_pcm_frames_s16__decode_mid_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); } break; case DRFLAC_CHANNEL_ASSIGNMENT_INDEPENDENT: default: { - if (pFlac->currentFrame.channelAssignment == 1) // 1 = Stereo - { - // Stereo optimized inner loop unroll. - const int* pDecodedSamples0 = pFlac->currentFrame.subframes[0].pDecodedSamples + firstAlignedSampleInFrame; - const int* pDecodedSamples1 = pFlac->currentFrame.subframes[1].pDecodedSamples + firstAlignedSampleInFrame; - - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - bufferOut[i*2+0] = pDecodedSamples0[i] >> rshift0; - bufferOut[i*2+1] = pDecodedSamples1[i] >> rshift1; - } - } - else - { - // Generic interleaving. - for (uint64_t i = 0; i < alignedSampleCountPerChannel; ++i) { - for (unsigned int j = 0; j < channelCount; ++j) { - bufferOut[(i*channelCount)+j] = (pFlac->currentFrame.subframes[j].pDecodedSamples[firstAlignedSampleInFrame + i]) >> (pFlac->currentFrame.subframes[j].wastedBitsPerSample - unusedBitsPerSample); - } - } - } + drflac_read_pcm_frames_s16__decode_independent_stereo(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); } break; } - } - - uint64_t alignedSamplesRead = alignedSampleCountPerChannel * channelCount; - samplesRead += alignedSamplesRead; - samplesReadFromFrameSoFar += alignedSamplesRead; - bufferOut += alignedSamplesRead; - samplesToRead -= alignedSamplesRead; - pFlac->currentFrame.samplesRemaining -= (unsigned int)alignedSamplesRead; - - - - // At this point we may still have some excess samples left to read. - if (samplesToRead > 0 && pFlac->currentFrame.samplesRemaining > 0) - { - uint64_t excessSamplesRead = 0; - if (samplesToRead < pFlac->currentFrame.samplesRemaining) { - excessSamplesRead = drflac__read_s16__misaligned(pFlac, samplesToRead, bufferOut); - } else { - excessSamplesRead = drflac__read_s16__misaligned(pFlac, pFlac->currentFrame.samplesRemaining, bufferOut); + } else { + /* Generic interleaving. */ + drflac_uint64 i; + for (i = 0; i < frameCountThisIteration; ++i) { + unsigned int j; + for (j = 0; j < channelCount; ++j) { + drflac_int32 sampleS32 = (drflac_int32)((drflac_uint32)(pFlac->currentFLACFrame.subframes[j].pSamplesS32[iFirstPCMFrame + i]) << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[j].wastedBitsPerSample)); + pBufferOut[(i*channelCount)+j] = (drflac_int16)(sampleS32 >> 16); + } } - - samplesRead += excessSamplesRead; - samplesReadFromFrameSoFar += excessSamplesRead; - bufferOut += excessSamplesRead; - samplesToRead -= excessSamplesRead; } + + framesRead += frameCountThisIteration; + pBufferOut += frameCountThisIteration * channelCount; + framesToRead -= frameCountThisIteration; + pFlac->currentPCMFrame += frameCountThisIteration; + pFlac->currentFLACFrame.pcmFramesRemaining -= (drflac_uint32)frameCountThisIteration; } } - return samplesRead; + return framesRead; } -static bool drflac_seek_to_sample(drflac* pFlac, uint64_t sampleIndex) + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 left = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (float)((drflac_int32)left / 2147483648.0); + pOutputSamples[i*2+1] = (float)((drflac_int32)right / 2147483648.0); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + float factor = 1 / 2147483648.0; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 left0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 left1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 left2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 left3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 right0 = left0 - side0; + drflac_uint32 right1 = left1 - side1; + drflac_uint32 right2 = left2 - side2; + drflac_uint32 right3 = left3 - side3; + + pOutputSamples[i*8+0] = (drflac_int32)left0 * factor; + pOutputSamples[i*8+1] = (drflac_int32)right0 * factor; + pOutputSamples[i*8+2] = (drflac_int32)left1 * factor; + pOutputSamples[i*8+3] = (drflac_int32)right1 * factor; + pOutputSamples[i*8+4] = (drflac_int32)left2 * factor; + pOutputSamples[i*8+5] = (drflac_int32)right2 * factor; + pOutputSamples[i*8+6] = (drflac_int32)left3 * factor; + pOutputSamples[i*8+7] = (drflac_int32)right3 * factor; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left * factor; + pOutputSamples[i*2+1] = (drflac_int32)right * factor; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + __m128 factor; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor = _mm_set1_ps(1.0f / 8388608.0f); + + for (i = 0; i < frameCount4; ++i) { + __m128i left = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i right = _mm_sub_epi32(left, side); + __m128 leftf = _mm_mul_ps(_mm_cvtepi32_ps(left), factor); + __m128 rightf = _mm_mul_ps(_mm_cvtepi32_ps(right), factor); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left / 8388608.0f; + pOutputSamples[i*2+1] = (drflac_int32)right / 8388608.0f; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + float32x4_t factor4; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor4 = vdupq_n_f32(1.0f / 8388608.0f); + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t left; + uint32x4_t side; + uint32x4_t right; + float32x4_t leftf; + float32x4_t rightf; + + left = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + right = vsubq_u32(left, side); + leftf = vmulq_f32(vcvtq_f32_s32(vreinterpretq_s32_u32(left)), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(vreinterpretq_s32_u32(right)), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 left = pInputSamples0U32[i] << shift0; + drflac_uint32 side = pInputSamples1U32[i] << shift1; + drflac_uint32 right = left - side; + + pOutputSamples[i*2+0] = (drflac_int32)left / 8388608.0f; + pOutputSamples[i*2+1] = (drflac_int32)right / 8388608.0f; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_left_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_left_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_left_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_f32__decode_left_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_f32__decode_left_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + for (i = 0; i < frameCount; ++i) { + drflac_uint32 side = (drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + drflac_uint32 right = (drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (float)((drflac_int32)left / 2147483648.0); + pOutputSamples[i*2+1] = (float)((drflac_int32)right / 2147483648.0); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + float factor = 1 / 2147483648.0; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 side0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 side1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 side2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 side3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 right0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 right1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 right2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 right3 = pInputSamples1U32[i*4+3] << shift1; + + drflac_uint32 left0 = right0 + side0; + drflac_uint32 left1 = right1 + side1; + drflac_uint32 left2 = right2 + side2; + drflac_uint32 left3 = right3 + side3; + + pOutputSamples[i*8+0] = (drflac_int32)left0 * factor; + pOutputSamples[i*8+1] = (drflac_int32)right0 * factor; + pOutputSamples[i*8+2] = (drflac_int32)left1 * factor; + pOutputSamples[i*8+3] = (drflac_int32)right1 * factor; + pOutputSamples[i*8+4] = (drflac_int32)left2 * factor; + pOutputSamples[i*8+5] = (drflac_int32)right2 * factor; + pOutputSamples[i*8+6] = (drflac_int32)left3 * factor; + pOutputSamples[i*8+7] = (drflac_int32)right3 * factor; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left * factor; + pOutputSamples[i*2+1] = (drflac_int32)right * factor; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + __m128 factor; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor = _mm_set1_ps(1.0f / 8388608.0f); + + for (i = 0; i < frameCount4; ++i) { + __m128i side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + __m128i right = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + __m128i left = _mm_add_epi32(right, side); + __m128 leftf = _mm_mul_ps(_mm_cvtepi32_ps(left), factor); + __m128 rightf = _mm_mul_ps(_mm_cvtepi32_ps(right), factor); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left / 8388608.0f; + pOutputSamples[i*2+1] = (drflac_int32)right / 8388608.0f; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + float32x4_t factor4; + int32x4_t shift0_4; + int32x4_t shift1_4; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor4 = vdupq_n_f32(1.0f / 8388608.0f); + shift0_4 = vdupq_n_s32(shift0); + shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + uint32x4_t side; + uint32x4_t right; + uint32x4_t left; + float32x4_t leftf; + float32x4_t rightf; + + side = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4); + right = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4); + left = vaddq_u32(right, side); + leftf = vmulq_f32(vcvtq_f32_s32(vreinterpretq_s32_u32(left)), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(vreinterpretq_s32_u32(right)), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 side = pInputSamples0U32[i] << shift0; + drflac_uint32 right = pInputSamples1U32[i] << shift1; + drflac_uint32 left = right + side; + + pOutputSamples[i*2+0] = (drflac_int32)left / 8388608.0f; + pOutputSamples[i*2+1] = (drflac_int32)right / 8388608.0f; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_right_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_right_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_right_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_f32__decode_right_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_f32__decode_right_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + drflac_uint32 mid = (drflac_uint32)pInputSamples0[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = (drflac_uint32)pInputSamples1[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (float)((((drflac_int32)(mid + side) >> 1) << (unusedBitsPerSample)) / 2147483648.0); + pOutputSamples[i*2+1] = (float)((((drflac_int32)(mid - side) >> 1) << (unusedBitsPerSample)) / 2147483648.0); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample; + float factor = 1 / 2147483648.0; + + if (shift > 0) { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (mid0 + side0) << shift; + temp1L = (mid1 + side1) << shift; + temp2L = (mid2 + side2) << shift; + temp3L = (mid3 + side3) << shift; + + temp0R = (mid0 - side0) << shift; + temp1R = (mid1 - side1) << shift; + temp2R = (mid2 - side2) << shift; + temp3R = (mid3 - side3) << shift; + + pOutputSamples[i*8+0] = (drflac_int32)temp0L * factor; + pOutputSamples[i*8+1] = (drflac_int32)temp0R * factor; + pOutputSamples[i*8+2] = (drflac_int32)temp1L * factor; + pOutputSamples[i*8+3] = (drflac_int32)temp1R * factor; + pOutputSamples[i*8+4] = (drflac_int32)temp2L * factor; + pOutputSamples[i*8+5] = (drflac_int32)temp2R * factor; + pOutputSamples[i*8+6] = (drflac_int32)temp3L * factor; + pOutputSamples[i*8+7] = (drflac_int32)temp3R * factor; + } + } else { + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 temp0L; + drflac_uint32 temp1L; + drflac_uint32 temp2L; + drflac_uint32 temp3L; + drflac_uint32 temp0R; + drflac_uint32 temp1R; + drflac_uint32 temp2R; + drflac_uint32 temp3R; + + drflac_uint32 mid0 = pInputSamples0U32[i*4+0] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid1 = pInputSamples0U32[i*4+1] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid2 = pInputSamples0U32[i*4+2] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 mid3 = pInputSamples0U32[i*4+3] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + + drflac_uint32 side0 = pInputSamples1U32[i*4+0] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side1 = pInputSamples1U32[i*4+1] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side2 = pInputSamples1U32[i*4+2] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + drflac_uint32 side3 = pInputSamples1U32[i*4+3] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid0 = (mid0 << 1) | (side0 & 0x01); + mid1 = (mid1 << 1) | (side1 & 0x01); + mid2 = (mid2 << 1) | (side2 & 0x01); + mid3 = (mid3 << 1) | (side3 & 0x01); + + temp0L = (drflac_uint32)((drflac_int32)(mid0 + side0) >> 1); + temp1L = (drflac_uint32)((drflac_int32)(mid1 + side1) >> 1); + temp2L = (drflac_uint32)((drflac_int32)(mid2 + side2) >> 1); + temp3L = (drflac_uint32)((drflac_int32)(mid3 + side3) >> 1); + + temp0R = (drflac_uint32)((drflac_int32)(mid0 - side0) >> 1); + temp1R = (drflac_uint32)((drflac_int32)(mid1 - side1) >> 1); + temp2R = (drflac_uint32)((drflac_int32)(mid2 - side2) >> 1); + temp3R = (drflac_uint32)((drflac_int32)(mid3 - side3) >> 1); + + pOutputSamples[i*8+0] = (drflac_int32)temp0L * factor; + pOutputSamples[i*8+1] = (drflac_int32)temp0R * factor; + pOutputSamples[i*8+2] = (drflac_int32)temp1L * factor; + pOutputSamples[i*8+3] = (drflac_int32)temp1R * factor; + pOutputSamples[i*8+4] = (drflac_int32)temp2L * factor; + pOutputSamples[i*8+5] = (drflac_int32)temp2R * factor; + pOutputSamples[i*8+6] = (drflac_int32)temp3L * factor; + pOutputSamples[i*8+7] = (drflac_int32)temp3R * factor; + } + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid + side) >> 1) << unusedBitsPerSample) * factor; + pOutputSamples[i*2+1] = (drflac_int32)((drflac_uint32)((drflac_int32)(mid - side) >> 1) << unusedBitsPerSample) * factor; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample - 8; + float factor; + __m128 factor128; + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor = 1.0f / 8388608.0f; + factor128 = _mm_set1_ps(factor); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i tempL; + __m128i tempR; + __m128 leftf; + __m128 rightf; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + tempL = _mm_srai_epi32(_mm_add_epi32(mid, side), 1); + tempR = _mm_srai_epi32(_mm_sub_epi32(mid, side), 1); + + leftf = _mm_mul_ps(_mm_cvtepi32_ps(tempL), factor128); + rightf = _mm_mul_ps(_mm_cvtepi32_ps(tempR), factor128); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = ((drflac_int32)(mid + side) >> 1) * factor; + pOutputSamples[i*2+1] = ((drflac_int32)(mid - side) >> 1) * factor; + } + } else { + shift -= 1; + for (i = 0; i < frameCount4; ++i) { + __m128i mid; + __m128i side; + __m128i tempL; + __m128i tempR; + __m128 leftf; + __m128 rightf; + + mid = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + side = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + mid = _mm_or_si128(_mm_slli_epi32(mid, 1), _mm_and_si128(side, _mm_set1_epi32(0x01))); + + tempL = _mm_slli_epi32(_mm_add_epi32(mid, side), shift); + tempR = _mm_slli_epi32(_mm_sub_epi32(mid, side), shift); + + leftf = _mm_mul_ps(_mm_cvtepi32_ps(tempL), factor128); + rightf = _mm_mul_ps(_mm_cvtepi32_ps(tempR), factor128); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((mid + side) << shift) * factor; + pOutputSamples[i*2+1] = (drflac_int32)((mid - side) << shift) * factor; + } + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift = unusedBitsPerSample - 8; + float factor; + float32x4_t factor4; + int32x4_t shift4; + int32x4_t wbps0_4; /* Wasted Bits Per Sample */ + int32x4_t wbps1_4; /* Wasted Bits Per Sample */ + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 24); + + factor = 1.0f / 8388608.0f; + factor4 = vdupq_n_f32(factor); + wbps0_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample); + wbps1_4 = vdupq_n_s32(pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample); + + if (shift == 0) { + for (i = 0; i < frameCount4; ++i) { + int32x4_t lefti; + int32x4_t righti; + float32x4_t leftf; + float32x4_t rightf; + + uint32x4_t mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbps0_4); + uint32x4_t side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbps1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, vdupq_n_u32(1))); + + lefti = vshrq_n_s32(vreinterpretq_s32_u32(vaddq_u32(mid, side)), 1); + righti = vshrq_n_s32(vreinterpretq_s32_u32(vsubq_u32(mid, side)), 1); + + leftf = vmulq_f32(vcvtq_f32_s32(lefti), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(righti), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = ((drflac_int32)(mid + side) >> 1) * factor; + pOutputSamples[i*2+1] = ((drflac_int32)(mid - side) >> 1) * factor; + } + } else { + shift -= 1; + shift4 = vdupq_n_s32(shift); + for (i = 0; i < frameCount4; ++i) { + uint32x4_t mid; + uint32x4_t side; + int32x4_t lefti; + int32x4_t righti; + float32x4_t leftf; + float32x4_t rightf; + + mid = vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), wbps0_4); + side = vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), wbps1_4); + + mid = vorrq_u32(vshlq_n_u32(mid, 1), vandq_u32(side, vdupq_n_u32(1))); + + lefti = vreinterpretq_s32_u32(vshlq_u32(vaddq_u32(mid, side), shift4)); + righti = vreinterpretq_s32_u32(vshlq_u32(vsubq_u32(mid, side), shift4)); + + leftf = vmulq_f32(vcvtq_f32_s32(lefti), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(righti), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + drflac_uint32 mid = pInputSamples0U32[i] << pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 side = pInputSamples1U32[i] << pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + + mid = (mid << 1) | (side & 0x01); + + pOutputSamples[i*2+0] = (drflac_int32)((mid + side) << shift) * factor; + pOutputSamples[i*2+1] = (drflac_int32)((mid - side) << shift) * factor; + } + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_mid_side(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_mid_side__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_mid_side__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_f32__decode_mid_side__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_f32__decode_mid_side__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + +#if 0 +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo__reference(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + for (drflac_uint64 i = 0; i < frameCount; ++i) { + pOutputSamples[i*2+0] = (float)((drflac_int32)((drflac_uint32)pInputSamples0[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample)) / 2147483648.0); + pOutputSamples[i*2+1] = (float)((drflac_int32)((drflac_uint32)pInputSamples1[i] << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample)) / 2147483648.0); + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo__scalar(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample; + drflac_uint32 shift1 = unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample; + float factor = 1 / 2147483648.0; + + for (i = 0; i < frameCount4; ++i) { + drflac_uint32 tempL0 = pInputSamples0U32[i*4+0] << shift0; + drflac_uint32 tempL1 = pInputSamples0U32[i*4+1] << shift0; + drflac_uint32 tempL2 = pInputSamples0U32[i*4+2] << shift0; + drflac_uint32 tempL3 = pInputSamples0U32[i*4+3] << shift0; + + drflac_uint32 tempR0 = pInputSamples1U32[i*4+0] << shift1; + drflac_uint32 tempR1 = pInputSamples1U32[i*4+1] << shift1; + drflac_uint32 tempR2 = pInputSamples1U32[i*4+2] << shift1; + drflac_uint32 tempR3 = pInputSamples1U32[i*4+3] << shift1; + + pOutputSamples[i*8+0] = (drflac_int32)tempL0 * factor; + pOutputSamples[i*8+1] = (drflac_int32)tempR0 * factor; + pOutputSamples[i*8+2] = (drflac_int32)tempL1 * factor; + pOutputSamples[i*8+3] = (drflac_int32)tempR1 * factor; + pOutputSamples[i*8+4] = (drflac_int32)tempL2 * factor; + pOutputSamples[i*8+5] = (drflac_int32)tempR2 * factor; + pOutputSamples[i*8+6] = (drflac_int32)tempL3 * factor; + pOutputSamples[i*8+7] = (drflac_int32)tempR3 * factor; + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0) * factor; + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1) * factor; + } +} + +#if defined(DRFLAC_SUPPORT_SSE2) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo__sse2(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + + float factor = 1.0f / 8388608.0f; + __m128 factor128 = _mm_set1_ps(factor); + + for (i = 0; i < frameCount4; ++i) { + __m128i lefti; + __m128i righti; + __m128 leftf; + __m128 rightf; + + lefti = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples0 + i), shift0); + righti = _mm_slli_epi32(_mm_loadu_si128((const __m128i*)pInputSamples1 + i), shift1); + + leftf = _mm_mul_ps(_mm_cvtepi32_ps(lefti), factor128); + rightf = _mm_mul_ps(_mm_cvtepi32_ps(righti), factor128); + + _mm_storeu_ps(pOutputSamples + i*8 + 0, _mm_unpacklo_ps(leftf, rightf)); + _mm_storeu_ps(pOutputSamples + i*8 + 4, _mm_unpackhi_ps(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0) * factor; + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1) * factor; + } +} +#endif + +#if defined(DRFLAC_SUPPORT_NEON) +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo__neon(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ + drflac_uint64 i; + drflac_uint64 frameCount4 = frameCount >> 2; + const drflac_uint32* pInputSamples0U32 = (const drflac_uint32*)pInputSamples0; + const drflac_uint32* pInputSamples1U32 = (const drflac_uint32*)pInputSamples1; + drflac_uint32 shift0 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[0].wastedBitsPerSample) - 8; + drflac_uint32 shift1 = (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[1].wastedBitsPerSample) - 8; + + float factor = 1.0f / 8388608.0f; + float32x4_t factor4 = vdupq_n_f32(factor); + int32x4_t shift0_4 = vdupq_n_s32(shift0); + int32x4_t shift1_4 = vdupq_n_s32(shift1); + + for (i = 0; i < frameCount4; ++i) { + int32x4_t lefti; + int32x4_t righti; + float32x4_t leftf; + float32x4_t rightf; + + lefti = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples0U32 + i*4), shift0_4)); + righti = vreinterpretq_s32_u32(vshlq_u32(vld1q_u32(pInputSamples1U32 + i*4), shift1_4)); + + leftf = vmulq_f32(vcvtq_f32_s32(lefti), factor4); + rightf = vmulq_f32(vcvtq_f32_s32(righti), factor4); + + drflac__vst2q_f32(pOutputSamples + i*8, vzipq_f32(leftf, rightf)); + } + + for (i = (frameCount4 << 2); i < frameCount; ++i) { + pOutputSamples[i*2+0] = (drflac_int32)(pInputSamples0U32[i] << shift0) * factor; + pOutputSamples[i*2+1] = (drflac_int32)(pInputSamples1U32[i] << shift1) * factor; + } +} +#endif + +static DRFLAC_INLINE void drflac_read_pcm_frames_f32__decode_independent_stereo(drflac* pFlac, drflac_uint64 frameCount, drflac_uint32 unusedBitsPerSample, const drflac_int32* pInputSamples0, const drflac_int32* pInputSamples1, float* pOutputSamples) +{ +#if defined(DRFLAC_SUPPORT_SSE2) + if (drflac__gIsSSE2Supported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_independent_stereo__sse2(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#elif defined(DRFLAC_SUPPORT_NEON) + if (drflac__gIsNEONSupported && pFlac->bitsPerSample <= 24) { + drflac_read_pcm_frames_f32__decode_independent_stereo__neon(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); + } else +#endif + { + /* Scalar fallback. */ +#if 0 + drflac_read_pcm_frames_f32__decode_independent_stereo__reference(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#else + drflac_read_pcm_frames_f32__decode_independent_stereo__scalar(pFlac, frameCount, unusedBitsPerSample, pInputSamples0, pInputSamples1, pOutputSamples); +#endif + } +} + +DRFLAC_API drflac_uint64 drflac_read_pcm_frames_f32(drflac* pFlac, drflac_uint64 framesToRead, float* pBufferOut) +{ + drflac_uint64 framesRead; + drflac_uint32 unusedBitsPerSample; + + if (pFlac == NULL || framesToRead == 0) { + return 0; + } + + if (pBufferOut == NULL) { + return drflac__seek_forward_by_pcm_frames(pFlac, framesToRead); + } + + DRFLAC_ASSERT(pFlac->bitsPerSample <= 32); + unusedBitsPerSample = 32 - pFlac->bitsPerSample; + + framesRead = 0; + while (framesToRead > 0) { + /* If we've run out of samples in this frame, go to the next. */ + if (pFlac->currentFLACFrame.pcmFramesRemaining == 0) { + if (!drflac__read_and_decode_next_flac_frame(pFlac)) { + break; /* Couldn't read the next frame, so just break from the loop and return. */ + } + } else { + unsigned int channelCount = drflac__get_channel_count_from_channel_assignment(pFlac->currentFLACFrame.header.channelAssignment); + drflac_uint64 iFirstPCMFrame = pFlac->currentFLACFrame.header.blockSizeInPCMFrames - pFlac->currentFLACFrame.pcmFramesRemaining; + drflac_uint64 frameCountThisIteration = framesToRead; + + if (frameCountThisIteration > pFlac->currentFLACFrame.pcmFramesRemaining) { + frameCountThisIteration = pFlac->currentFLACFrame.pcmFramesRemaining; + } + + if (channelCount == 2) { + const drflac_int32* pDecodedSamples0 = pFlac->currentFLACFrame.subframes[0].pSamplesS32 + iFirstPCMFrame; + const drflac_int32* pDecodedSamples1 = pFlac->currentFLACFrame.subframes[1].pSamplesS32 + iFirstPCMFrame; + + switch (pFlac->currentFLACFrame.header.channelAssignment) + { + case DRFLAC_CHANNEL_ASSIGNMENT_LEFT_SIDE: + { + drflac_read_pcm_frames_f32__decode_left_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_RIGHT_SIDE: + { + drflac_read_pcm_frames_f32__decode_right_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_MID_SIDE: + { + drflac_read_pcm_frames_f32__decode_mid_side(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + + case DRFLAC_CHANNEL_ASSIGNMENT_INDEPENDENT: + default: + { + drflac_read_pcm_frames_f32__decode_independent_stereo(pFlac, frameCountThisIteration, unusedBitsPerSample, pDecodedSamples0, pDecodedSamples1, pBufferOut); + } break; + } + } else { + /* Generic interleaving. */ + drflac_uint64 i; + for (i = 0; i < frameCountThisIteration; ++i) { + unsigned int j; + for (j = 0; j < channelCount; ++j) { + drflac_int32 sampleS32 = (drflac_int32)((drflac_uint32)(pFlac->currentFLACFrame.subframes[j].pSamplesS32[iFirstPCMFrame + i]) << (unusedBitsPerSample + pFlac->currentFLACFrame.subframes[j].wastedBitsPerSample)); + pBufferOut[(i*channelCount)+j] = (float)(sampleS32 / 2147483648.0); + } + } + } + + framesRead += frameCountThisIteration; + pBufferOut += frameCountThisIteration * channelCount; + framesToRead -= frameCountThisIteration; + pFlac->currentPCMFrame += frameCountThisIteration; + pFlac->currentFLACFrame.pcmFramesRemaining -= (unsigned int)frameCountThisIteration; + } + } + + return framesRead; +} + + +DRFLAC_API drflac_bool32 drflac_seek_to_pcm_frame(drflac* pFlac, drflac_uint64 pcmFrameIndex) { if (pFlac == NULL) { - return false; + return DRFLAC_FALSE; } - if (sampleIndex == 0) { + /* Don't do anything if we're already on the seek point. */ + if (pFlac->currentPCMFrame == pcmFrameIndex) { + return DRFLAC_TRUE; + } + + /* + If we don't know where the first frame begins then we can't seek. This will happen when the STREAMINFO block was not present + when the decoder was opened. + */ + if (pFlac->firstFLACFramePosInBytes == 0) { + return DRFLAC_FALSE; + } + + if (pcmFrameIndex == 0) { + pFlac->currentPCMFrame = 0; return drflac__seek_to_first_frame(pFlac); + } else { + drflac_bool32 wasSuccessful = DRFLAC_FALSE; + drflac_uint64 originalPCMFrame = pFlac->currentPCMFrame; + + /* Clamp the sample to the end. */ + if (pcmFrameIndex > pFlac->totalPCMFrameCount) { + pcmFrameIndex = pFlac->totalPCMFrameCount; + } + + /* If the target sample and the current sample are in the same frame we just move the position forward. */ + if (pcmFrameIndex > pFlac->currentPCMFrame) { + /* Forward. */ + drflac_uint32 offset = (drflac_uint32)(pcmFrameIndex - pFlac->currentPCMFrame); + if (pFlac->currentFLACFrame.pcmFramesRemaining > offset) { + pFlac->currentFLACFrame.pcmFramesRemaining -= offset; + pFlac->currentPCMFrame = pcmFrameIndex; + return DRFLAC_TRUE; + } + } else { + /* Backward. */ + drflac_uint32 offsetAbs = (drflac_uint32)(pFlac->currentPCMFrame - pcmFrameIndex); + drflac_uint32 currentFLACFramePCMFrameCount = pFlac->currentFLACFrame.header.blockSizeInPCMFrames; + drflac_uint32 currentFLACFramePCMFramesConsumed = currentFLACFramePCMFrameCount - pFlac->currentFLACFrame.pcmFramesRemaining; + if (currentFLACFramePCMFramesConsumed > offsetAbs) { + pFlac->currentFLACFrame.pcmFramesRemaining += offsetAbs; + pFlac->currentPCMFrame = pcmFrameIndex; + return DRFLAC_TRUE; + } + } + + /* + Different techniques depending on encapsulation. Using the native FLAC seektable with Ogg encapsulation is a bit awkward so + we'll instead use Ogg's natural seeking facility. + */ +#ifndef DR_FLAC_NO_OGG + if (pFlac->container == drflac_container_ogg) + { + wasSuccessful = drflac_ogg__seek_to_pcm_frame(pFlac, pcmFrameIndex); + } + else +#endif + { + /* First try seeking via the seek table. If this fails, fall back to a brute force seek which is much slower. */ + if (/*!wasSuccessful && */!pFlac->_noSeekTableSeek) { + wasSuccessful = drflac__seek_to_pcm_frame__seek_table(pFlac, pcmFrameIndex); + } + +#if !defined(DR_FLAC_NO_CRC) + /* Fall back to binary search if seek table seeking fails. This requires the length of the stream to be known. */ + if (!wasSuccessful && !pFlac->_noBinarySearchSeek && pFlac->totalPCMFrameCount > 0) { + wasSuccessful = drflac__seek_to_pcm_frame__binary_search(pFlac, pcmFrameIndex); + } +#endif + + /* Fall back to brute force if all else fails. */ + if (!wasSuccessful && !pFlac->_noBruteForceSeek) { + wasSuccessful = drflac__seek_to_pcm_frame__brute_force(pFlac, pcmFrameIndex); + } + } + + if (wasSuccessful) { + pFlac->currentPCMFrame = pcmFrameIndex; + } else { + /* Seek failed. Try putting the decoder back to it's original state. */ + if (drflac_seek_to_pcm_frame(pFlac, originalPCMFrame) == DRFLAC_FALSE) { + /* Failed to seek back to the original PCM frame. Fall back to 0. */ + drflac_seek_to_pcm_frame(pFlac, 0); + } + } + + return wasSuccessful; } - - // Clamp the sample to the end. - if (sampleIndex >= pFlac->totalSampleCount) { - sampleIndex = pFlac->totalSampleCount - 1; - } - - - // First try seeking via the seek table. If this fails, fall back to a brute force seek which is much slower. - if (!drflac__seek_to_sample__seek_table(pFlac, sampleIndex)) { - return drflac__seek_to_sample__brute_force(pFlac, sampleIndex); - } - - return true; } -#endif //DR_FLAC_IMPLEMENTATION + +/* High Level APIs */ + +/* SIZE_MAX */ +#if defined(SIZE_MAX) + #define DRFLAC_SIZE_MAX SIZE_MAX +#else + #if defined(DRFLAC_64BIT) + #define DRFLAC_SIZE_MAX ((drflac_uint64)0xFFFFFFFFFFFFFFFF) + #else + #define DRFLAC_SIZE_MAX 0xFFFFFFFF + #endif +#endif +/* End SIZE_MAX */ + + +/* Using a macro as the definition of the drflac__full_decode_and_close_*() API family. Sue me. */ +#define DRFLAC_DEFINE_FULL_READ_AND_CLOSE(extension, type) \ +static type* drflac__full_read_and_close_ ## extension (drflac* pFlac, unsigned int* channelsOut, unsigned int* sampleRateOut, drflac_uint64* totalPCMFrameCountOut)\ +{ \ + type* pSampleData = NULL; \ + drflac_uint64 totalPCMFrameCount; \ + type buffer[4096]; \ + drflac_uint64 pcmFramesRead; \ + size_t sampleDataBufferSize = sizeof(buffer); \ + \ + DRFLAC_ASSERT(pFlac != NULL); \ + \ + totalPCMFrameCount = 0; \ + \ + pSampleData = (type*)drflac__malloc_from_callbacks(sampleDataBufferSize, &pFlac->allocationCallbacks); \ + if (pSampleData == NULL) { \ + goto on_error; \ + } \ + \ + while ((pcmFramesRead = (drflac_uint64)drflac_read_pcm_frames_##extension(pFlac, sizeof(buffer)/sizeof(buffer[0])/pFlac->channels, buffer)) > 0) { \ + if (((totalPCMFrameCount + pcmFramesRead) * pFlac->channels * sizeof(type)) > sampleDataBufferSize) { \ + type* pNewSampleData; \ + size_t newSampleDataBufferSize; \ + \ + newSampleDataBufferSize = sampleDataBufferSize * 2; \ + pNewSampleData = (type*)drflac__realloc_from_callbacks(pSampleData, newSampleDataBufferSize, sampleDataBufferSize, &pFlac->allocationCallbacks); \ + if (pNewSampleData == NULL) { \ + drflac__free_from_callbacks(pSampleData, &pFlac->allocationCallbacks); \ + goto on_error; \ + } \ + \ + sampleDataBufferSize = newSampleDataBufferSize; \ + pSampleData = pNewSampleData; \ + } \ + \ + DRFLAC_COPY_MEMORY(pSampleData + (totalPCMFrameCount*pFlac->channels), buffer, (size_t)(pcmFramesRead*pFlac->channels*sizeof(type))); \ + totalPCMFrameCount += pcmFramesRead; \ + } \ + \ + /* At this point everything should be decoded, but we just want to fill the unused part buffer with silence - need to \ + protect those ears from random noise! */ \ + DRFLAC_ZERO_MEMORY(pSampleData + (totalPCMFrameCount*pFlac->channels), (size_t)(sampleDataBufferSize - totalPCMFrameCount*pFlac->channels*sizeof(type))); \ + \ + if (sampleRateOut) *sampleRateOut = pFlac->sampleRate; \ + if (channelsOut) *channelsOut = pFlac->channels; \ + if (totalPCMFrameCountOut) *totalPCMFrameCountOut = totalPCMFrameCount; \ + \ + drflac_close(pFlac); \ + return pSampleData; \ + \ +on_error: \ + drflac_close(pFlac); \ + return NULL; \ +} + +DRFLAC_DEFINE_FULL_READ_AND_CLOSE(s32, drflac_int32) +DRFLAC_DEFINE_FULL_READ_AND_CLOSE(s16, drflac_int16) +DRFLAC_DEFINE_FULL_READ_AND_CLOSE(f32, float) + +DRFLAC_API drflac_int32* drflac_open_and_read_pcm_frames_s32(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drflac_uint64* totalPCMFrameCountOut, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalPCMFrameCountOut) { + *totalPCMFrameCountOut = 0; + } + + pFlac = drflac_open(onRead, onSeek, onTell, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s32(pFlac, channelsOut, sampleRateOut, totalPCMFrameCountOut); +} + +DRFLAC_API drflac_int16* drflac_open_and_read_pcm_frames_s16(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drflac_uint64* totalPCMFrameCountOut, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalPCMFrameCountOut) { + *totalPCMFrameCountOut = 0; + } + + pFlac = drflac_open(onRead, onSeek, onTell, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s16(pFlac, channelsOut, sampleRateOut, totalPCMFrameCountOut); +} + +DRFLAC_API float* drflac_open_and_read_pcm_frames_f32(drflac_read_proc onRead, drflac_seek_proc onSeek, drflac_tell_proc onTell, void* pUserData, unsigned int* channelsOut, unsigned int* sampleRateOut, drflac_uint64* totalPCMFrameCountOut, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (channelsOut) { + *channelsOut = 0; + } + if (sampleRateOut) { + *sampleRateOut = 0; + } + if (totalPCMFrameCountOut) { + *totalPCMFrameCountOut = 0; + } + + pFlac = drflac_open(onRead, onSeek, onTell, pUserData, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_f32(pFlac, channelsOut, sampleRateOut, totalPCMFrameCountOut); +} + +#ifndef DR_FLAC_NO_STDIO +DRFLAC_API drflac_int32* drflac_open_file_and_read_pcm_frames_s32(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_file(filename, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s32(pFlac, channels, sampleRate, totalPCMFrameCount); +} + +DRFLAC_API drflac_int16* drflac_open_file_and_read_pcm_frames_s16(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_file(filename, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s16(pFlac, channels, sampleRate, totalPCMFrameCount); +} + +DRFLAC_API float* drflac_open_file_and_read_pcm_frames_f32(const char* filename, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_file(filename, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_f32(pFlac, channels, sampleRate, totalPCMFrameCount); +} +#endif + +DRFLAC_API drflac_int32* drflac_open_memory_and_read_pcm_frames_s32(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_memory(data, dataSize, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s32(pFlac, channels, sampleRate, totalPCMFrameCount); +} + +DRFLAC_API drflac_int16* drflac_open_memory_and_read_pcm_frames_s16(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_memory(data, dataSize, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_s16(pFlac, channels, sampleRate, totalPCMFrameCount); +} + +DRFLAC_API float* drflac_open_memory_and_read_pcm_frames_f32(const void* data, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, drflac_uint64* totalPCMFrameCount, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + drflac* pFlac; + + if (sampleRate) { + *sampleRate = 0; + } + if (channels) { + *channels = 0; + } + if (totalPCMFrameCount) { + *totalPCMFrameCount = 0; + } + + pFlac = drflac_open_memory(data, dataSize, pAllocationCallbacks); + if (pFlac == NULL) { + return NULL; + } + + return drflac__full_read_and_close_f32(pFlac, channels, sampleRate, totalPCMFrameCount); +} + + +DRFLAC_API void drflac_free(void* p, const drflac_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + drflac__free_from_callbacks(p, pAllocationCallbacks); + } else { + drflac__free_default(p, NULL); + } +} + + + + +DRFLAC_API void drflac_init_vorbis_comment_iterator(drflac_vorbis_comment_iterator* pIter, drflac_uint32 commentCount, const void* pComments) +{ + if (pIter == NULL) { + return; + } + + pIter->countRemaining = commentCount; + pIter->pRunningData = (const char*)pComments; +} + +DRFLAC_API const char* drflac_next_vorbis_comment(drflac_vorbis_comment_iterator* pIter, drflac_uint32* pCommentLengthOut) +{ + drflac_int32 length; + const char* pComment; + + /* Safety. */ + if (pCommentLengthOut) { + *pCommentLengthOut = 0; + } + + if (pIter == NULL || pIter->countRemaining == 0 || pIter->pRunningData == NULL) { + return NULL; + } + + length = drflac__le2host_32_ptr_unaligned(pIter->pRunningData); + pIter->pRunningData += 4; + + pComment = pIter->pRunningData; + pIter->pRunningData += length; + pIter->countRemaining -= 1; + + if (pCommentLengthOut) { + *pCommentLengthOut = length; + } + + return pComment; +} + + + + +DRFLAC_API void drflac_init_cuesheet_track_iterator(drflac_cuesheet_track_iterator* pIter, drflac_uint32 trackCount, const void* pTrackData) +{ + if (pIter == NULL) { + return; + } + + pIter->countRemaining = trackCount; + pIter->pRunningData = (const char*)pTrackData; +} + +DRFLAC_API drflac_bool32 drflac_next_cuesheet_track(drflac_cuesheet_track_iterator* pIter, drflac_cuesheet_track* pCuesheetTrack) +{ + drflac_cuesheet_track cuesheetTrack; + const char* pRunningData; + drflac_uint64 offsetHi; + drflac_uint64 offsetLo; + + if (pIter == NULL || pIter->countRemaining == 0 || pIter->pRunningData == NULL) { + return DRFLAC_FALSE; + } + + pRunningData = pIter->pRunningData; + + offsetHi = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4; + offsetLo = drflac__be2host_32(*(const drflac_uint32*)pRunningData); pRunningData += 4; + cuesheetTrack.offset = offsetLo | (offsetHi << 32); + cuesheetTrack.trackNumber = pRunningData[0]; pRunningData += 1; + DRFLAC_COPY_MEMORY(cuesheetTrack.ISRC, pRunningData, sizeof(cuesheetTrack.ISRC)); pRunningData += 12; + cuesheetTrack.isAudio = (pRunningData[0] & 0x80) != 0; + cuesheetTrack.preEmphasis = (pRunningData[0] & 0x40) != 0; pRunningData += 14; + cuesheetTrack.indexCount = pRunningData[0]; pRunningData += 1; + cuesheetTrack.pIndexPoints = (const drflac_cuesheet_track_index*)pRunningData; pRunningData += cuesheetTrack.indexCount * sizeof(drflac_cuesheet_track_index); + + pIter->pRunningData = pRunningData; + pIter->countRemaining -= 1; + + if (pCuesheetTrack) { + *pCuesheetTrack = cuesheetTrack; + } + + return DRFLAC_TRUE; +} + +#if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic pop +#endif +#endif /* dr_flac_c */ +#endif /* DR_FLAC_IMPLEMENTATION */ /* +REVISION HISTORY +================ +v0.13.2 - 2025-12-02 + - Improve robustness of the parsing of picture metadata to improve support for memory constrained embedded devices. + - Fix a warning about an assigned by unused variable. + - Improvements to drflac_open_and_read_pcm_frames_*() and family to avoid excessively large memory allocations from malformed files. + +v0.13.1 - 2025-09-10 + - Fix an error with the NXDK build. + +v0.13.0 - 2025-07-23 + - API CHANGE: Seek origin enums have been renamed to match the naming convention used by other dr_libs libraries: + - drflac_seek_origin_start -> DRFLAC_SEEK_SET + - drflac_seek_origin_current -> DRFLAC_SEEK_CUR + - DRFLAC_SEEK_END (new) + - API CHANGE: A new seek origin has been added to allow seeking from the end of the file. If you implement your own `onSeek` callback, you should now detect and handle `DRFLAC_SEEK_END`. If seeking to the end is not supported, return `DRFLAC_FALSE`. If you only use `*_open_file()` or `*_open_memory()`, you need not change anything. + - API CHANGE: An `onTell` callback has been added to the following functions: + - drflac_open() + - drflac_open_relaxed() + - drflac_open_with_metadata() + - drflac_open_with_metadata_relaxed() + - drflac_open_and_read_pcm_frames_s32() + - drflac_open_and_read_pcm_frames_s16() + - drflac_open_and_read_pcm_frames_f32() + - Fix compilation for AIX OS. + +v0.12.43 - 2024-12-17 + - Fix a possible buffer overflow during decoding. + - Improve detection of ARM64EC + +v0.12.42 - 2023-11-02 + - Fix build for ARMv6-M. + - Fix a compilation warning with GCC. + +v0.12.41 - 2023-06-17 + - Fix an incorrect date in revision history. No functional change. + +v0.12.40 - 2023-05-22 + - Minor code restructure. No functional change. + +v0.12.39 - 2022-09-17 + - Fix compilation with DJGPP. + - Fix compilation error with Visual Studio 2019 and the ARM build. + - Fix an error with SSE 4.1 detection. + - Add support for disabling wchar_t with DR_WAV_NO_WCHAR. + - Improve compatibility with compilers which lack support for explicit struct packing. + - Improve compatibility with low-end and embedded hardware by reducing the amount of stack + allocation when loading an Ogg encapsulated file. + +v0.12.38 - 2022-04-10 + - Fix compilation error on older versions of GCC. + +v0.12.37 - 2022-02-12 + - Improve ARM detection. + +v0.12.36 - 2022-02-07 + - Fix a compilation error with the ARM build. + +v0.12.35 - 2022-02-06 + - Fix a bug due to underestimating the amount of precision required for the prediction stage. + - Fix some bugs found from fuzz testing. + +v0.12.34 - 2022-01-07 + - Fix some misalignment bugs when reading metadata. + +v0.12.33 - 2021-12-22 + - Fix a bug with seeking when the seek table does not start at PCM frame 0. + +v0.12.32 - 2021-12-11 + - Fix a warning with Clang. + +v0.12.31 - 2021-08-16 + - Silence some warnings. + +v0.12.30 - 2021-07-31 + - Fix platform detection for ARM64. + +v0.12.29 - 2021-04-02 + - Fix a bug where the running PCM frame index is set to an invalid value when over-seeking. + - Fix a decoding error due to an incorrect validation check. + +v0.12.28 - 2021-02-21 + - Fix a warning due to referencing _MSC_VER when it is undefined. + +v0.12.27 - 2021-01-31 + - Fix a static analysis warning. + +v0.12.26 - 2021-01-17 + - Fix a compilation warning due to _BSD_SOURCE being deprecated. + +v0.12.25 - 2020-12-26 + - Update documentation. + +v0.12.24 - 2020-11-29 + - Fix ARM64/NEON detection when compiling with MSVC. + +v0.12.23 - 2020-11-21 + - Fix compilation with OpenWatcom. + +v0.12.22 - 2020-11-01 + - Fix an error with the previous release. + +v0.12.21 - 2020-11-01 + - Fix a possible deadlock when seeking. + - Improve compiler support for older versions of GCC. + +v0.12.20 - 2020-09-08 + - Fix a compilation error on older compilers. + +v0.12.19 - 2020-08-30 + - Fix a bug due to an undefined 32-bit shift. + +v0.12.18 - 2020-08-14 + - Fix a crash when compiling with clang-cl. + +v0.12.17 - 2020-08-02 + - Simplify sized types. + +v0.12.16 - 2020-07-25 + - Fix a compilation warning. + +v0.12.15 - 2020-07-06 + - Check for negative LPC shifts and return an error. + +v0.12.14 - 2020-06-23 + - Add include guard for the implementation section. + +v0.12.13 - 2020-05-16 + - Add compile-time and run-time version querying. + - DRFLAC_VERSION_MINOR + - DRFLAC_VERSION_MAJOR + - DRFLAC_VERSION_REVISION + - DRFLAC_VERSION_STRING + - drflac_version() + - drflac_version_string() + +v0.12.12 - 2020-04-30 + - Fix compilation errors with VC6. + +v0.12.11 - 2020-04-19 + - Fix some pedantic warnings. + - Fix some undefined behaviour warnings. + +v0.12.10 - 2020-04-10 + - Fix some bugs when trying to seek with an invalid seek table. + +v0.12.9 - 2020-04-05 + - Fix warnings. + +v0.12.8 - 2020-04-04 + - Add drflac_open_file_w() and drflac_open_file_with_metadata_w(). + - Fix some static analysis warnings. + - Minor documentation updates. + +v0.12.7 - 2020-03-14 + - Fix compilation errors with VC6. + +v0.12.6 - 2020-03-07 + - Fix compilation error with Visual Studio .NET 2003. + +v0.12.5 - 2020-01-30 + - Silence some static analysis warnings. + +v0.12.4 - 2020-01-29 + - Silence some static analysis warnings. + +v0.12.3 - 2019-12-02 + - Fix some warnings when compiling with GCC and the -Og flag. + - Fix a crash in out-of-memory situations. + - Fix potential integer overflow bug. + - Fix some static analysis warnings. + - Fix a possible crash when using custom memory allocators without a custom realloc() implementation. + - Fix a bug with binary search seeking where the bits per sample is not a multiple of 8. + +v0.12.2 - 2019-10-07 + - Internal code clean up. + +v0.12.1 - 2019-09-29 + - Fix some Clang Static Analyzer warnings. + - Fix an unused variable warning. + +v0.12.0 - 2019-09-23 + - API CHANGE: Add support for user defined memory allocation routines. This system allows the program to specify their own memory allocation + routines with a user data pointer for client-specific contextual data. This adds an extra parameter to the end of the following APIs: + - drflac_open() + - drflac_open_relaxed() + - drflac_open_with_metadata() + - drflac_open_with_metadata_relaxed() + - drflac_open_file() + - drflac_open_file_with_metadata() + - drflac_open_memory() + - drflac_open_memory_with_metadata() + - drflac_open_and_read_pcm_frames_s32() + - drflac_open_and_read_pcm_frames_s16() + - drflac_open_and_read_pcm_frames_f32() + - drflac_open_file_and_read_pcm_frames_s32() + - drflac_open_file_and_read_pcm_frames_s16() + - drflac_open_file_and_read_pcm_frames_f32() + - drflac_open_memory_and_read_pcm_frames_s32() + - drflac_open_memory_and_read_pcm_frames_s16() + - drflac_open_memory_and_read_pcm_frames_f32() + Set this extra parameter to NULL to use defaults which is the same as the previous behaviour. Setting this NULL will use + DRFLAC_MALLOC, DRFLAC_REALLOC and DRFLAC_FREE. + - Remove deprecated APIs: + - drflac_read_s32() + - drflac_read_s16() + - drflac_read_f32() + - drflac_seek_to_sample() + - drflac_open_and_decode_s32() + - drflac_open_and_decode_s16() + - drflac_open_and_decode_f32() + - drflac_open_and_decode_file_s32() + - drflac_open_and_decode_file_s16() + - drflac_open_and_decode_file_f32() + - drflac_open_and_decode_memory_s32() + - drflac_open_and_decode_memory_s16() + - drflac_open_and_decode_memory_f32() + - Remove drflac.totalSampleCount which is now replaced with drflac.totalPCMFrameCount. You can emulate drflac.totalSampleCount + by doing pFlac->totalPCMFrameCount*pFlac->channels. + - Rename drflac.currentFrame to drflac.currentFLACFrame to remove ambiguity with PCM frames. + - Fix errors when seeking to the end of a stream. + - Optimizations to seeking. + - SSE improvements and optimizations. + - ARM NEON optimizations. + - Optimizations to drflac_read_pcm_frames_s16(). + - Optimizations to drflac_read_pcm_frames_s32(). + +v0.11.10 - 2019-06-26 + - Fix a compiler error. + +v0.11.9 - 2019-06-16 + - Silence some ThreadSanitizer warnings. + +v0.11.8 - 2019-05-21 + - Fix warnings. + +v0.11.7 - 2019-05-06 + - C89 fixes. + +v0.11.6 - 2019-05-05 + - Add support for C89. + - Fix a compiler warning when CRC is disabled. + - Change license to choice of public domain or MIT-0. + +v0.11.5 - 2019-04-19 + - Fix a compiler error with GCC. + +v0.11.4 - 2019-04-17 + - Fix some warnings with GCC when compiling with -std=c99. + +v0.11.3 - 2019-04-07 + - Silence warnings with GCC. + +v0.11.2 - 2019-03-10 + - Fix a warning. + +v0.11.1 - 2019-02-17 + - Fix a potential bug with seeking. + +v0.11.0 - 2018-12-16 + - API CHANGE: Deprecated drflac_read_s32(), drflac_read_s16() and drflac_read_f32() and replaced them with + drflac_read_pcm_frames_s32(), drflac_read_pcm_frames_s16() and drflac_read_pcm_frames_f32(). The new APIs take + and return PCM frame counts instead of sample counts. To upgrade you will need to change the input count by + dividing it by the channel count, and then do the same with the return value. + - API_CHANGE: Deprecated drflac_seek_to_sample() and replaced with drflac_seek_to_pcm_frame(). Same rules as + the changes to drflac_read_*() apply. + - API CHANGE: Deprecated drflac_open_and_decode_*() and replaced with drflac_open_*_and_read_*(). Same rules as + the changes to drflac_read_*() apply. + - Optimizations. + +v0.10.0 - 2018-09-11 + - Remove the DR_FLAC_NO_WIN32_IO option and the Win32 file IO functionality. If you need to use Win32 file IO you + need to do it yourself via the callback API. + - Fix the clang build. + - Fix undefined behavior. + - Fix errors with CUESHEET metdata blocks. + - Add an API for iterating over each cuesheet track in the CUESHEET metadata block. This works the same way as the + Vorbis comment API. + - Other miscellaneous bug fixes, mostly relating to invalid FLAC streams. + - Minor optimizations. + +v0.9.11 - 2018-08-29 + - Fix a bug with sample reconstruction. + +v0.9.10 - 2018-08-07 + - Improve 64-bit detection. + +v0.9.9 - 2018-08-05 + - Fix C++ build on older versions of GCC. + +v0.9.8 - 2018-07-24 + - Fix compilation errors. + +v0.9.7 - 2018-07-05 + - Fix a warning. + +v0.9.6 - 2018-06-29 + - Fix some typos. + +v0.9.5 - 2018-06-23 + - Fix some warnings. + +v0.9.4 - 2018-06-14 + - Optimizations to seeking. + - Clean up. + +v0.9.3 - 2018-05-22 + - Bug fix. + +v0.9.2 - 2018-05-12 + - Fix a compilation error due to a missing break statement. + +v0.9.1 - 2018-04-29 + - Fix compilation error with Clang. + +v0.9 - 2018-04-24 + - Fix Clang build. + - Start using major.minor.revision versioning. + +v0.8g - 2018-04-19 + - Fix build on non-x86/x64 architectures. + +v0.8f - 2018-02-02 + - Stop pretending to support changing rate/channels mid stream. + +v0.8e - 2018-02-01 + - Fix a crash when the block size of a frame is larger than the maximum block size defined by the FLAC stream. + - Fix a crash the the Rice partition order is invalid. + +v0.8d - 2017-09-22 + - Add support for decoding streams with ID3 tags. ID3 tags are just skipped. + +v0.8c - 2017-09-07 + - Fix warning on non-x86/x64 architectures. + +v0.8b - 2017-08-19 + - Fix build on non-x86/x64 architectures. + +v0.8a - 2017-08-13 + - A small optimization for the Clang build. + +v0.8 - 2017-08-12 + - API CHANGE: Rename dr_* types to drflac_*. + - Optimizations. This brings dr_flac back to about the same class of efficiency as the reference implementation. + - Add support for custom implementations of malloc(), realloc(), etc. + - Add CRC checking to Ogg encapsulated streams. + - Fix VC++ 6 build. This is only for the C++ compiler. The C compiler is not currently supported. + - Bug fixes. + +v0.7 - 2017-07-23 + - Add support for opening a stream without a header block. To do this, use drflac_open_relaxed() / drflac_open_with_metadata_relaxed(). + +v0.6 - 2017-07-22 + - Add support for recovering from invalid frames. With this change, dr_flac will simply skip over invalid frames as if they + never existed. Frames are checked against their sync code, the CRC-8 of the frame header and the CRC-16 of the whole frame. + +v0.5 - 2017-07-16 + - Fix typos. + - Change drflac_bool* types to unsigned. + - Add CRC checking. This makes dr_flac slower, but can be disabled with #define DR_FLAC_NO_CRC. + +v0.4f - 2017-03-10 + - Fix a couple of bugs with the bitstreaming code. + +v0.4e - 2017-02-17 + - Fix some warnings. + +v0.4d - 2016-12-26 + - Add support for 32-bit floating-point PCM decoding. + - Use drflac_int* and drflac_uint* sized types to improve compiler support. + - Minor improvements to documentation. + +v0.4c - 2016-12-26 + - Add support for signed 16-bit integer PCM decoding. + +v0.4b - 2016-10-23 + - A minor change to drflac_bool8 and drflac_bool32 types. + +v0.4a - 2016-10-11 + - Rename drBool32 to drflac_bool32 for styling consistency. + +v0.4 - 2016-09-29 + - API/ABI CHANGE: Use fixed size 32-bit booleans instead of the built-in bool type. + - API CHANGE: Rename drflac_open_and_decode*() to drflac_open_and_decode*_s32(). + - API CHANGE: Swap the order of "channels" and "sampleRate" parameters in drflac_open_and_decode*(). Rationale for this is to + keep it consistent with drflac_audio. + +v0.3f - 2016-09-21 + - Fix a warning with GCC. + +v0.3e - 2016-09-18 + - Fixed a bug where GCC 4.3+ was not getting properly identified. + - Fixed a few typos. + - Changed date formats to ISO 8601 (YYYY-MM-DD). + +v0.3d - 2016-06-11 + - Minor clean up. + +v0.3c - 2016-05-28 + - Fixed compilation error. + +v0.3b - 2016-05-16 + - Fixed Linux/GCC build. + - Updated documentation. + +v0.3a - 2016-05-15 + - Minor fixes to documentation. + +v0.3 - 2016-05-11 + - Optimizations. Now at about parity with the reference implementation on 32-bit builds. + - Lots of clean up. + +v0.2b - 2016-05-10 + - Bug fixes. + +v0.2a - 2016-05-10 + - Made drflac_open_and_decode() more robust. + - Removed an unused debugging variable + +v0.2 - 2016-05-09 + - Added support for Ogg encapsulation. + - API CHANGE. Have the onSeek callback take a third argument which specifies whether or not the seek + should be relative to the start or the current position. Also changes the seeking rules such that + seeking offsets will never be negative. + - Have drflac_open_and_decode() fail gracefully if the stream has an unknown total sample count. + +v0.1b - 2016-05-07 + - Properly close the file handle in drflac_open_file() and family when the decoder fails to initialize. + - Removed a stale comment. + +v0.1a - 2016-05-05 + - Minor formatting changes. + - Fixed a warning on the GCC build. + +v0.1 - 2016-05-03 + - Initial versioned release. +*/ + +/* +This software is available as a choice of the following licenses. Choose +whichever you prefer. + +=============================================================================== +ALTERNATIVE 1 - Public Domain (www.unlicense.org) +=============================================================================== This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to + +=============================================================================== +ALTERNATIVE 2 - MIT No Attribution +=============================================================================== +Copyright 2023 David Reid + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ diff --git a/panda/src/movies/flacAudio.h b/panda/src/movies/flacAudio.h index 5309e95eae..b999ebe004 100644 --- a/panda/src/movies/flacAudio.h +++ b/panda/src/movies/flacAudio.h @@ -20,7 +20,7 @@ class FlacAudioCursor; /** - * Reads FLAC audio files. Ogg-encapsulated FLAC files are not supported. + * Reads FLAC audio files. Ogg-encapsulated FLAC files are now supported. * * @since 1.10.0 */ diff --git a/panda/src/movies/flacAudioCursor.cxx b/panda/src/movies/flacAudioCursor.cxx index 1c9d06984b..0927f54b5d 100644 --- a/panda/src/movies/flacAudioCursor.cxx +++ b/panda/src/movies/flacAudioCursor.cxx @@ -26,7 +26,7 @@ extern "C" { */ static size_t cb_read_proc(void *user, void *buffer, size_t size) { std::istream *stream = (std::istream *)user; - nassertr(stream != nullptr, false); + nassertr(stream != nullptr, 0); stream->read((char *)buffer, size); @@ -39,14 +39,37 @@ static size_t cb_read_proc(void *user, void *buffer, size_t size) { } /** - * Callback passed to dr_flac to implement file I/O via the VirtualFileSystem. + * Callback passed to dr_flac to implement file seeking via the VirtualFileSystem. */ -static bool cb_seek_proc(void *user, int offset) { - std::istream *stream = (std::istream *)user; - nassertr(stream != nullptr, false); +static drflac_bool32 cb_seek_proc(void* user, int offset, drflac_seek_origin origin) { + std::istream* stream = static_cast(user); + nassertr(stream != nullptr, DRFLAC_FALSE); - stream->seekg(offset, std::ios::cur); - return !stream->fail(); + std::ios_base::seekdir dir; + switch (origin) { + case DRFLAC_SEEK_SET: + dir = std::ios_base::beg; + break; + case DRFLAC_SEEK_CUR: + dir = std::ios_base::cur; + break; + default: + return DRFLAC_FALSE; + } + + stream->seekg(offset, dir); + return !stream->fail() ? DRFLAC_TRUE : DRFLAC_FALSE; +} + +/** + * Callback passed to dr_flac to report the current stream position. + */ +static drflac_bool32 cb_tell_proc(void *user, drflac_int64 *pCursor) { + std::istream *stream = (std::istream *)user; + nassertr(stream != nullptr, DRFLAC_FALSE); + + *pCursor = (drflac_int64)stream->tellg(); + return !stream->fail() ? DRFLAC_TRUE : DRFLAC_FALSE; } TypeHandle FlacAudioCursor::_type_handle; @@ -65,7 +88,7 @@ FlacAudioCursor(FlacAudio *src, std::istream *stream) : nassertv(stream != nullptr); nassertv(stream->good()); - _drflac = drflac_open(&cb_read_proc, &cb_seek_proc, (void *)stream); + _drflac = drflac_open(&cb_read_proc, &cb_seek_proc, &cb_tell_proc, (void *)stream, nullptr); if (_drflac == nullptr) { movies_cat.error() @@ -73,7 +96,7 @@ FlacAudioCursor(FlacAudio *src, std::istream *stream) : _is_valid = false; } - _length = (_drflac->totalSampleCount / _drflac->channels) / (double)_drflac->sampleRate; + _length = _drflac->totalPCMFrameCount / (double)_drflac->sampleRate; _audio_channels = _drflac->channels; _audio_rate = _drflac->sampleRate; @@ -107,7 +130,7 @@ seek(double t) { uint64_t sample = t * _drflac->sampleRate; - if (drflac_seek_to_sample(_drflac, sample * _drflac->channels)) { + if (drflac_seek_to_pcm_frame(_drflac, sample)) { _last_seek = sample / (double)_drflac->sampleRate; _samples_read = 0; } @@ -120,8 +143,7 @@ seek(double t) { */ int FlacAudioCursor:: read_samples(int n, int16_t *data) { - int desired = n * _audio_channels; - n = drflac_read_s16(_drflac, desired, data) / _audio_channels; + n = (int)drflac_read_pcm_frames_s16(_drflac, n, data); _samples_read += n; return n; } From 97de7863e4d4661e7c244361c76a08cf1be7af72 Mon Sep 17 00:00:00 2001 From: TRIGON69 <65608179+TRIGON69@users.noreply.github.com> Date: Tue, 30 Dec 2025 01:33:48 +0530 Subject: [PATCH 243/291] direct: Fix DirectGrid alpha issue and getVertexColor error (#1793) Fixes #1292 --- direct/src/directtools/DirectGeometry.py | 4 ++-- direct/src/directtools/DirectGrid.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/direct/src/directtools/DirectGeometry.py b/direct/src/directtools/DirectGeometry.py index 141d263c40..86ee2646b2 100644 --- a/direct/src/directtools/DirectGeometry.py +++ b/direct/src/directtools/DirectGeometry.py @@ -79,8 +79,8 @@ class LineNodePath(NodePath): def getVertex(self, index): return self.lineSegs.getVertex(index) - def getVertexColor(self): - return self.lineSegs.getVertexColor() + def getVertexColor(self, index): + return self.lineSegs.getVertexColor(index) def drawArrow(self, sv, ev, arrowAngle, arrowLength): """ diff --git a/direct/src/directtools/DirectGrid.py b/direct/src/directtools/DirectGrid.py index 33c6ac58dc..b1c578d692 100644 --- a/direct/src/directtools/DirectGrid.py +++ b/direct/src/directtools/DirectGrid.py @@ -33,7 +33,7 @@ class DirectGrid(NodePath, DirectObject): self.centerLines = LineNodePath(self.lines) self.centerLines.lineNode.setName('centerLines') - self.centerLines.setColor(VBase4(1, 0, 0, 0)) + self.centerLines.setColor(VBase4(1, 0, 0, 1)) self.centerLines.setThickness(3) # Small marker to hilight snap-to-grid point From 52829ed610d7bf4d393e43c9f948d393a6921f9f Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 30 Dec 2025 23:18:25 +0100 Subject: [PATCH 244/291] workflow: Disable OpenAL on macOS for now for test build --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69558d097f..c86b5b0053 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -396,7 +396,7 @@ jobs: tar -xf panda3d-1.10.16-tools-mac.tar.gz mv panda3d-1.10.16/thirdparty thirdparty rmdir panda3d-1.10.16 - (cd thirdparty/darwin-libs-a && rm -rf rocket) + (cd thirdparty/darwin-libs-a && rm -rf rocket openal) - name: Set up XCode (macOS) if: runner.os == 'macOS' From b4b18724bd1b363e77fcd42a703f2645916835de Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 30 Dec 2025 23:18:46 +0100 Subject: [PATCH 245/291] dtoolutil: Improve error messages for iostream wrappers --- dtool/src/dtoolutil/iostream_ext.cxx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dtool/src/dtoolutil/iostream_ext.cxx b/dtool/src/dtoolutil/iostream_ext.cxx index 459f95c229..08272da20f 100644 --- a/dtool/src/dtoolutil/iostream_ext.cxx +++ b/dtool/src/dtoolutil/iostream_ext.cxx @@ -134,8 +134,9 @@ readinto(PyObject *b) { Py_buffer view; if (PyObject_GetBuffer(b, &view, PyBUF_CONTIG) == -1) { - PyErr_SetString(PyExc_TypeError, - "write() requires a contiguous, read-write bytes-like object"); + PyErr_Format(PyExc_TypeError, + "readinto() requires a contiguous, read-write bytes-like object, not '%.100s'", + Py_TYPE(b)->tp_name); return 0; } @@ -260,7 +261,9 @@ write(PyObject *b) { Py_buffer view; if (PyObject_GetBuffer(b, &view, PyBUF_CONTIG_RO) == -1) { - PyErr_SetString(PyExc_TypeError, "write() requires a contiguous buffer"); + PyErr_Format(PyExc_TypeError, + "write() requires a bytes-like object, not '%.100s'", + Py_TYPE(b)->tp_name); return; } From 3d3c2e7b7c70a190cd82bd2662c8c8eb0ceb5d25 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 31 Dec 2025 20:00:09 +0100 Subject: [PATCH 246/291] text: Give default font a default name This is mostly so that the texture gets a name other than "_0" --- panda/src/text/textProperties.cxx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/panda/src/text/textProperties.cxx b/panda/src/text/textProperties.cxx index 1cbc8ddb9a..a61ba92b96 100644 --- a/panda/src/text/textProperties.cxx +++ b/panda/src/text/textProperties.cxx @@ -489,4 +489,8 @@ load_default_font() { #endif // HAVE_FREETYPE #endif // COMPILE_IN_DEFAULT_FONT + + if (_default_font != nullptr && !_default_font->has_name()) { + _default_font->set_name("default_font"); + } } From 02bd38820e5dffe3bc249dea12ae7a770234a1db Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 31 Dec 2025 20:03:42 +0100 Subject: [PATCH 247/291] build: Update to interrogate 0.8.1 --- dtool/Config.cmake | 2 +- makepanda/makepandacore.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dtool/Config.cmake b/dtool/Config.cmake index 35fcd93484..de8568ce8a 100644 --- a/dtool/Config.cmake +++ b/dtool/Config.cmake @@ -265,7 +265,7 @@ if(BUILD_INTERROGATE) panda3d-interrogate GIT_REPOSITORY https://github.com/panda3d/interrogate.git - GIT_TAG ccbd5f9d79037a9d53d6dfb1a5e9fdf30f2510c1 + GIT_TAG 7cf2550d2c8d95b8c268aa4bb0b5602e85a086dc PREFIX ${_interrogate_dir} CMAKE_ARGS diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 9879469b25..1e32c11dd3 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -589,8 +589,8 @@ def GetInterrogateDir(): return INTERROGATE_DIR dir = os.path.join(GetOutputDir(), "tmp", "interrogate") - if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.7.1.dist-info")): - oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.7.1" % (sys.executable, dir)) + if not os.path.isdir(os.path.join(dir, "panda3d_interrogate-0.8.1.dist-info")): + oscmd("\"%s\" -m pip install --force-reinstall --upgrade -t \"%s\" panda3d-interrogate==0.8.1" % (sys.executable, dir)) INTERROGATE_DIR = dir From b806035c401b2d5c8c96872ad0856020e50ba2d6 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 31 Dec 2025 20:06:03 +0100 Subject: [PATCH 248/291] putil: Switch to new Python 3.13+ public long APIs for BitArray --- panda/src/putil/bitArray_ext.cxx | 39 ++++++++++++++++++++++++++++++++ tests/putil/test_bitarray.py | 14 ++++++++++++ 2 files changed, 53 insertions(+) diff --git a/panda/src/putil/bitArray_ext.cxx b/panda/src/putil/bitArray_ext.cxx index b454593d1b..1391e37e92 100644 --- a/panda/src/putil/bitArray_ext.cxx +++ b/panda/src/putil/bitArray_ext.cxx @@ -20,6 +20,29 @@ */ void Extension:: __init__(PyObject *init_value) { +#if PY_VERSION_HEX >= 0x030e0000 // Python 3.14 + BitArray::WordType word = 0; + Py_ssize_t result = PyLong_AsNativeBytes(init_value, &word, sizeof(word), Py_ASNATIVEBYTES_LITTLE_ENDIAN | Py_ASNATIVEBYTES_UNSIGNED_BUFFER | Py_ASNATIVEBYTES_REJECT_NEGATIVE); + if (result < 0) { + return; + } + + if ((size_t)result <= sizeof(BitArray::WordType)) { + // It fit in a single word + _this->_array.clear(); + if (word != 0) { + _this->_array.push_back(word); + } + } else { + // Requires multiple words. + size_t num_words = ((size_t)result + sizeof(BitArray::WordType) - 1) / sizeof(BitArray::WordType); + _this->_array.resize(num_words); + + if (PyLong_AsNativeBytes(init_value, &_this->_array[0], num_words * sizeof(BitArray::WordType), Py_ASNATIVEBYTES_LITTLE_ENDIAN | Py_ASNATIVEBYTES_UNSIGNED_BUFFER | Py_ASNATIVEBYTES_REJECT_NEGATIVE) <= 0) { + return; + } + } +#else if (!PyLong_Check(init_value) || !PyLong_IsNonNegative(init_value)) { PyErr_SetString(PyExc_ValueError, "BitArray constructor requires a positive integer"); return; @@ -38,6 +61,7 @@ __init__(PyObject *init_value) { #endif ); } +#endif } /** @@ -55,19 +79,34 @@ __getstate__() const { } if (_this->_highest_bits == 0) { +#if PY_VERSION_HEX >= 0x030e0000 // Python 3.14 + return PyLong_FromUnsignedNativeBytes( + &_this->_array[0], _this->_array.size() * sizeof(BitArray::WordType), + Py_ASNATIVEBYTES_LITTLE_ENDIAN); +#else return _PyLong_FromByteArray( (const unsigned char *)&_this->_array[0], _this->_array.size() * sizeof(BitArray::WordType), 1, 0); +#endif } else { // This is an infinite array, so we invert it to make it a finite array and // store it as an inverted long. BitArray copy(*_this); copy.invert_in_place(); +#if PY_VERSION_HEX >= 0x030e0000 // Python 3.14 + PyObject *state = PyLong_FromUnsignedNativeBytes( + ©._array[0], copy._array.size() * sizeof(BitArray::WordType), + Py_ASNATIVEBYTES_LITTLE_ENDIAN); +#else PyObject *state = _PyLong_FromByteArray( (const unsigned char *)©._array[0], copy._array.size() * sizeof(BitArray::WordType), 1, 0); +#endif + if (state == nullptr) { + return nullptr; + } PyObject *inverted = PyNumber_Invert(state); Py_DECREF(state); return inverted; diff --git a/tests/putil/test_bitarray.py b/tests/putil/test_bitarray.py index 81f058f904..15319edcb7 100644 --- a/tests/putil/test_bitarray.py +++ b/tests/putil/test_bitarray.py @@ -11,6 +11,20 @@ def test_bitarray_constructor(): assert BitArray().is_zero() assert BitArray(0).is_zero() + one = BitArray(1) + assert not one.is_zero() + assert one.get_lowest_on_bit() == 0 + assert one.get_highest_on_bit() == 0 + assert one.get_lowest_off_bit() == 1 + assert one.get_highest_off_bit() == -1 + + two = BitArray(2) + assert not two.is_zero() + assert two.get_lowest_on_bit() == 1 + assert two.get_highest_on_bit() == 1 + assert two.get_lowest_off_bit() == 0 + assert two.get_highest_off_bit() == -1 + ba = BitArray(0x10000000000000000000000000) assert ba.get_lowest_on_bit() == 100 assert ba.get_highest_on_bit() == 100 From 86b130458897f57b842924734198fcdd80f5f8aa Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 31 Dec 2025 20:10:19 +0100 Subject: [PATCH 249/291] display: Support extracting only subset of SSBO --- panda/src/display/graphicsEngine.cxx | 7 ++++--- panda/src/display/graphicsEngine.h | 3 ++- panda/src/display/graphicsStateGuardian.cxx | 3 ++- panda/src/display/graphicsStateGuardian.h | 3 ++- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 13 ++++++++++--- panda/src/glstuff/glGraphicsStateGuardian_src.h | 3 ++- tests/display/test_glsl_shader.py | 9 +++++++-- 7 files changed, 29 insertions(+), 12 deletions(-) diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx index 474f8e2934..8cb83e5a03 100644 --- a/panda/src/display/graphicsEngine.cxx +++ b/panda/src/display/graphicsEngine.cxx @@ -1145,15 +1145,16 @@ extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg) { * * This is mainly useful for debugging. It is a very slow call because it * introduces a pipeline stall both of Panda's pipeline and the graphics - * pipeline. + * pipeline. This is even when only a small amount of data is downloaded. * * The return value is empty if some kind of error occurred. */ vector_uchar GraphicsEngine:: -extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg) { +extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg, + size_t start, size_t size) { return run_on_draw_thread([=] () { vector_uchar data; - if (!gsg->extract_shader_buffer_data(buffer, data)) { + if (!gsg->extract_shader_buffer_data(buffer, data, start, size)) { data.clear(); } return data; diff --git a/panda/src/display/graphicsEngine.h b/panda/src/display/graphicsEngine.h index 88874f8e50..bd19d8d265 100644 --- a/panda/src/display/graphicsEngine.h +++ b/panda/src/display/graphicsEngine.h @@ -115,7 +115,8 @@ PUBLISHED: BLOCKING void flip_frame(); bool extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg); - vector_uchar extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg); + vector_uchar extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg, + size_t start = 0, size_t size = (size_t)-1); void dispatch_compute(const LVecBase3i &work_groups, const RenderState *state, GraphicsStateGuardian *gsg); diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 16518d79b4..19fe696d04 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -774,7 +774,8 @@ release_shader_buffers(const pvector &contexts) { * current contents synchronously. */ bool GraphicsStateGuardian:: -extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data) { +extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, + size_t start, size_t size) { return false; } diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index 9cb390f88c..8e9775b25d 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -326,7 +326,8 @@ public: virtual BufferContext *prepare_shader_buffer(ShaderBuffer *data); virtual void release_shader_buffer(BufferContext *ibc); virtual void release_shader_buffers(const pvector &contexts); - virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data); + virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, + size_t start = 0, size_t size = (size_t)-1); virtual void begin_occlusion_query(); virtual PT(OcclusionQueryContext) end_occlusion_query(); diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 034b5e5434..abf6f4cd0a 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -7766,14 +7766,21 @@ release_shader_buffers(const pvector &contexts) { * current contents synchronously. */ bool CLP(GraphicsStateGuardian):: -extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data) { +extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, + size_t start, size_t size) { BufferContext *bc = buffer->prepare_now(get_prepared_objects(), this); if (bc == nullptr || !bc->is_of_type(CLP(BufferContext)::get_class_type())) { return false; } CLP(BufferContext) *gbc = DCAST(CLP(BufferContext), bc); - data.resize(buffer->get_data_size_bytes()); + size_t total_size = buffer->get_data_size_bytes(); + if (start >= total_size) { + data.clear(); + return true; + } + + data.resize(std::min(total_size - start, size)); if (_glMemoryBarrier != nullptr) { _glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); @@ -7781,7 +7788,7 @@ extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data) { _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); - _glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, data.size(), &data[0]); + _glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, start, data.size(), &data[0]); _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); _current_sbuffer_index = 0; diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index a489599672..a037ba27df 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -399,7 +399,8 @@ public: CLP(BufferContext) *apply_shader_buffer(GLuint base, ShaderBuffer *buffer); virtual void release_shader_buffer(BufferContext *bc); virtual void release_shader_buffers(const pvector &contexts); - virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data); + virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, + size_t start, size_t size); #endif #ifndef OPENGLES diff --git a/tests/display/test_glsl_shader.py b/tests/display/test_glsl_shader.py index d6dd9ff5a5..3e0a3bc7be 100644 --- a/tests/display/test_glsl_shader.py +++ b/tests/display/test_glsl_shader.py @@ -301,7 +301,7 @@ def test_glsl_uimage(gsg): def test_glsl_ssbo(gsg): from struct import pack, unpack num1 = pack(' Date: Wed, 31 Dec 2025 21:22:12 +0100 Subject: [PATCH 250/291] gsg: Use DSA for extracting shader buffer data --- .../src/glstuff/glGraphicsStateGuardian_src.cxx | 17 +++++++++++------ panda/src/glstuff/glGraphicsStateGuardian_src.h | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index abf6f4cd0a..30188f912b 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -2834,6 +2834,9 @@ reset() { get_extension_func("glMapNamedBufferRange"); } + _glGetNamedBufferSubData = (PFNGLGETNAMEDBUFFERSUBDATAPROC) + get_extension_func("glGetNamedBufferSubData"); + _supports_dsa = true; } else { _supports_dsa = false; @@ -7786,12 +7789,14 @@ extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, _glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); } - _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); - - _glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, start, data.size(), &data[0]); - - _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - _current_sbuffer_index = 0; + if (_supports_dsa) { + _glGetNamedBufferSubData(gbc->_index, start, data.size(), &data[0]); + } else { + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); + _glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, start, data.size(), &data[0]); + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + _current_sbuffer_index = 0; + } report_my_gl_errors(); return true; diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index a037ba27df..e0ed36c30a 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -1019,6 +1019,7 @@ public: PFNGLGENERATETEXTUREMIPMAPPROC _glGenerateTextureMipmap; PFNGLBINDTEXTUREUNITPROC _glBindTextureUnit; PFNGLMAPNAMEDBUFFERRANGEPROC _glMapNamedBufferRange; + PFNGLGETNAMEDBUFFERSUBDATAPROC _glGetNamedBufferSubData; #endif #ifndef OPENGLES_1 From a9aa25577af639aa35da752d6510043e6195f478 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 31 Dec 2025 21:23:25 +0100 Subject: [PATCH 251/291] glgsg: Support updating SSBO data from the CPU (only UH_dynamic) --- panda/src/display/graphicsEngine.cxx | 22 ++++++++++ panda/src/display/graphicsEngine.h | 3 ++ panda/src/display/graphicsStateGuardian.cxx | 13 ++++++ panda/src/display/graphicsStateGuardian.h | 2 + .../glstuff/glGraphicsStateGuardian_src.cxx | 40 +++++++++++++++++++ .../src/glstuff/glGraphicsStateGuardian_src.h | 3 ++ tests/display/test_glsl_shader.py | 17 +++++++- 7 files changed, 98 insertions(+), 2 deletions(-) diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx index 8cb83e5a03..c8fcf9500f 100644 --- a/panda/src/display/graphicsEngine.cxx +++ b/panda/src/display/graphicsEngine.cxx @@ -1161,6 +1161,28 @@ extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg, }); } +/** + * Updates shader buffer data from the CPU. This may be a subsection of the + * buffer. Returns true on success. + * + * Updating is only allowed for buffers with usage hint set to UH_dynamic. + */ +bool GraphicsEngine:: +update_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg, + const vector_uchar &data, size_t offset) { + if (buffer->get_usage_hint() != GeomEnums::UH_dynamic) { + display_cat.error() + << "Cannot update ShaderBuffer with usage hint " << buffer->get_usage_hint() << "\n"; + return false; + } + + const unsigned char *data_ptr = data.data(); + const size_t data_size = data.size(); + return run_on_draw_thread([=] () { + return gsg->update_shader_buffer_data(buffer, offset, data_size, data_ptr); + }); +} + /** * Asks the indicated GraphicsStateGuardian to dispatch the compute shader in * the given ShaderAttrib using the given work group counts. This can act as diff --git a/panda/src/display/graphicsEngine.h b/panda/src/display/graphicsEngine.h index bd19d8d265..9c982364c0 100644 --- a/panda/src/display/graphicsEngine.h +++ b/panda/src/display/graphicsEngine.h @@ -117,6 +117,9 @@ PUBLISHED: bool extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg); vector_uchar extract_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg, size_t start = 0, size_t size = (size_t)-1); + bool update_shader_buffer_data(ShaderBuffer *buffer, GraphicsStateGuardian *gsg, + const vector_uchar &data, size_t offset = 0); + void dispatch_compute(const LVecBase3i &work_groups, const RenderState *state, GraphicsStateGuardian *gsg); diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 19fe696d04..11ec2540dd 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -766,6 +766,19 @@ release_shader_buffers(const pvector &contexts) { } } +/** + * This method should only be called by the GraphicsEngine. Do not call it + * directly; call GraphicsEngine::update_shader_buffer_data() instead. + * + * This method will be called in the draw thread to upload data to (a part of) + * the shader buffer from the CPU. + */ +bool GraphicsStateGuardian:: +update_shader_buffer_data(ShaderBuffer *buffer, size_t start, size_t size, + const unsigned char *data) { + return false; +} + /** * This method should only be called by the GraphicsEngine. Do not call it * directly; call GraphicsEngine::extract_texture_data() instead. diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index 8e9775b25d..36ffcb0960 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -326,6 +326,8 @@ public: virtual BufferContext *prepare_shader_buffer(ShaderBuffer *data); virtual void release_shader_buffer(BufferContext *ibc); virtual void release_shader_buffers(const pvector &contexts); + virtual bool update_shader_buffer_data(ShaderBuffer *buffer, size_t start, + size_t size, const unsigned char *data); virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, size_t start = 0, size_t size = (size_t)-1); diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 30188f912b..61ec11513d 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -2834,6 +2834,8 @@ reset() { get_extension_func("glMapNamedBufferRange"); } + _glNamedBufferSubData = (PFNGLNAMEDBUFFERSUBDATAPROC) + get_extension_func("glNamedBufferSubData"); _glGetNamedBufferSubData = (PFNGLGETNAMEDBUFFERSUBDATAPROC) get_extension_func("glGetNamedBufferSubData"); @@ -7613,6 +7615,9 @@ prepare_shader_buffer(ShaderBuffer *data) { if (data->get_usage_hint() == GeomEnums::UH_client) { flags |= GL_CLIENT_STORAGE_BIT; } + if (data->get_usage_hint() == GeomEnums::UH_dynamic) { + flags |= GL_DYNAMIC_STORAGE_BIT; + } _glBufferStorage(GL_SHADER_STORAGE_BUFFER, num_bytes, data->get_initial_data(), flags); } else { _glBufferData(GL_SHADER_STORAGE_BUFFER, num_bytes, data->get_initial_data(), get_usage(data->get_usage_hint())); @@ -7761,6 +7766,41 @@ release_shader_buffers(const pvector &contexts) { report_my_gl_errors(); } +/** + * This method should only be called by the GraphicsEngine. Do not call it + * directly; call GraphicsEngine::update_shader_buffer_data() instead. + * + * This method will be called in the draw thread to upload data to (a part of) + * the shader buffer from the CPU. + */ +bool CLP(GraphicsStateGuardian):: +update_shader_buffer_data(ShaderBuffer *buffer, size_t start, size_t size, + const unsigned char *data) { + nassertr(buffer->get_usage_hint() == GeomEnums::UH_dynamic, false); + + BufferContext *bc = buffer->prepare_now(get_prepared_objects(), this); + if (bc == nullptr || !bc->is_of_type(CLP(BufferContext)::get_class_type())) { + return false; + } + CLP(BufferContext) *gbc = DCAST(CLP(BufferContext), bc); + + if (_glMemoryBarrier != nullptr) { + _glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); + } + + if (_supports_dsa) { + _glNamedBufferSubData(gbc->_index, start, size, data); + } else { + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); + _glBufferSubData(GL_SHADER_STORAGE_BUFFER, start, size, data); + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + _current_sbuffer_index = 0; + } + report_my_gl_errors(); + + return false; +} + /** * This method should only be called by the GraphicsEngine. Do not call it * directly; call GraphicsEngine::extract_texture_data() instead. diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index e0ed36c30a..bd0eff59d3 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -399,6 +399,8 @@ public: CLP(BufferContext) *apply_shader_buffer(GLuint base, ShaderBuffer *buffer); virtual void release_shader_buffer(BufferContext *bc); virtual void release_shader_buffers(const pvector &contexts); + virtual bool update_shader_buffer_data(ShaderBuffer *buffer, size_t start, + size_t size, const unsigned char *data); virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, size_t start, size_t size); #endif @@ -1019,6 +1021,7 @@ public: PFNGLGENERATETEXTUREMIPMAPPROC _glGenerateTextureMipmap; PFNGLBINDTEXTUREUNITPROC _glBindTextureUnit; PFNGLMAPNAMEDBUFFERRANGEPROC _glMapNamedBufferRange; + PFNGLNAMEDBUFFERSUBDATAPROC _glNamedBufferSubData; PFNGLGETNAMEDBUFFERSUBDATAPROC _glGetNamedBufferSubData; #endif diff --git a/tests/display/test_glsl_shader.py b/tests/display/test_glsl_shader.py index 3e0a3bc7be..941e60f061 100644 --- a/tests/display/test_glsl_shader.py +++ b/tests/display/test_glsl_shader.py @@ -302,8 +302,8 @@ def test_glsl_ssbo(gsg): from struct import pack, unpack num1 = pack(' Date: Wed, 31 Dec 2025 21:26:37 +0100 Subject: [PATCH 252/291] putil: Add missing pointerTo.h include to callbackObject.h --- panda/src/putil/callbackObject.h | 1 + 1 file changed, 1 insertion(+) diff --git a/panda/src/putil/callbackObject.h b/panda/src/putil/callbackObject.h index 3e1b551eec..c51e1bae0d 100644 --- a/panda/src/putil/callbackObject.h +++ b/panda/src/putil/callbackObject.h @@ -16,6 +16,7 @@ #include "pandabase.h" #include "typedReferenceCount.h" +#include "pointerTo.h" class CallbackData; From cb03554da537c5eef4c3f5da018cb38cc7a3b1bc Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 31 Dec 2025 23:01:43 +0100 Subject: [PATCH 253/291] putil: Support vector_uchar to be passed to an event parameter --- direct/src/showbase/EventManager.py | 2 ++ dtool/src/dtoolbase/pvector.h | 10 +++++----- dtool/src/dtoolbase/register_type.cxx | 2 ++ dtool/src/dtoolbase/register_type.h | 14 ++++++++++++++ dtool/src/dtoolutil/vector_uchar.cxx | 22 ++++++++++++++++++++++ dtool/src/dtoolutil/vector_uchar.h | 3 +++ panda/src/event/eventParameter.I | 27 +++++++++++++++++++++++++++ panda/src/event/eventParameter.h | 3 +++ panda/src/putil/config_putil.cxx | 1 + panda/src/putil/paramValue.cxx | 1 + panda/src/putil/paramValue.h | 3 +++ 11 files changed, 83 insertions(+), 5 deletions(-) diff --git a/direct/src/showbase/EventManager.py b/direct/src/showbase/EventManager.py index 7bbb915915..14465120cb 100644 --- a/direct/src/showbase/EventManager.py +++ b/direct/src/showbase/EventManager.py @@ -76,6 +76,8 @@ class EventManager: return eventParameter.getStringValue() elif eventParameter.isWstring(): return eventParameter.getWstringValue() + elif hasattr(eventParameter, 'isBytes') and eventParameter.isBytes(): + return eventParameter.getBytesValue() elif eventParameter.isTypedRefCount(): return eventParameter.getTypedRefCountValue() elif eventParameter.isEmpty(): diff --git a/dtool/src/dtoolbase/pvector.h b/dtool/src/dtoolbase/pvector.h index 12e7c108ef..f8dcb21071 100644 --- a/dtool/src/dtoolbase/pvector.h +++ b/dtool/src/dtoolbase/pvector.h @@ -46,15 +46,15 @@ public: typedef std::vector base_class; typedef typename base_class::size_type size_type; - explicit pvector(TypeHandle type_handle = pvector_type_handle) : base_class(allocator(type_handle)) { } + explicit pvector(TypeHandle type_handle = get_type_handle(pvector)) : base_class(allocator(type_handle)) { } pvector(const pvector ©) : base_class(copy) { } pvector(pvector &&from) noexcept : base_class(std::move(from)) {}; - explicit pvector(size_type n, TypeHandle type_handle = pvector_type_handle) : base_class(n, Type(), allocator(type_handle)) { } - explicit pvector(size_type n, const Type &value, TypeHandle type_handle = pvector_type_handle) : base_class(n, value, allocator(type_handle)) { } - pvector(const Type *begin, const Type *end, TypeHandle type_handle = pvector_type_handle) : base_class(allocator(type_handle)) { + explicit pvector(size_type n, TypeHandle type_handle = get_type_handle(pvector)) : base_class(n, Type(), allocator(type_handle)) { } + explicit pvector(size_type n, const Type &value, TypeHandle type_handle = get_type_handle(pvector)) : base_class(n, value, allocator(type_handle)) { } + pvector(const Type *begin, const Type *end, TypeHandle type_handle = get_type_handle(pvector)) : base_class(allocator(type_handle)) { this->insert(this->end(), begin, end); } - pvector(std::initializer_list init, TypeHandle type_handle = pvector_type_handle) : base_class(std::move(init), allocator(type_handle)) { } + pvector(std::initializer_list init, TypeHandle type_handle = get_type_handle(pvector)) : base_class(std::move(init), allocator(type_handle)) { } pvector &operator =(const pvector ©) { base_class::operator =(copy); diff --git a/dtool/src/dtoolbase/register_type.cxx b/dtool/src/dtoolbase/register_type.cxx index b67475e4f6..649c4ebe11 100644 --- a/dtool/src/dtoolbase/register_type.cxx +++ b/dtool/src/dtoolbase/register_type.cxx @@ -26,6 +26,7 @@ TypeHandle double_type_handle; TypeHandle float_type_handle; TypeHandle string_type_handle; TypeHandle wstring_type_handle; +TypeHandle vector_uchar_type_handle; TypeHandle long_p_type_handle; TypeHandle int_p_type_handle; @@ -59,6 +60,7 @@ void init_system_type_handles() { register_type(float_type_handle, "float"); register_type(string_type_handle, "string"); register_type(wstring_type_handle, "wstring"); + register_type(vector_uchar_type_handle, "vector_uchar"); register_type(int_p_type_handle, "int*"); register_type(short_p_type_handle, "short*"); diff --git a/dtool/src/dtoolbase/register_type.h b/dtool/src/dtoolbase/register_type.h index a15cc9ddb1..95b2c48548 100644 --- a/dtool/src/dtoolbase/register_type.h +++ b/dtool/src/dtoolbase/register_type.h @@ -19,6 +19,9 @@ #include "typeHandle.h" #include "typeRegistry.h" +template +class pvector; + /** * This inline function is just a convenient way to call * TypeRegistry::register_type(), along with zero to four @@ -88,6 +91,7 @@ extern TypeHandle EXPCL_DTOOL_DTOOLBASE double_type_handle; extern TypeHandle EXPCL_DTOOL_DTOOLBASE float_type_handle; extern TypeHandle EXPCL_DTOOL_DTOOLBASE string_type_handle; extern TypeHandle EXPCL_DTOOL_DTOOLBASE wstring_type_handle; +extern TypeHandle EXPCL_DTOOL_DTOOLBASE vector_uchar_type_handle; extern TypeHandle long_p_type_handle; extern TypeHandle int_p_type_handle; @@ -175,6 +179,16 @@ INLINE TypeHandle _get_type_handle(const std::wstring *) { return wstring_type_handle; } +template +INLINE TypeHandle _get_type_handle(const pvector *) { + return pvector_type_handle; +} + +template<> +INLINE TypeHandle _get_type_handle(const pvector *) { + return vector_uchar_type_handle; +} + template<> INLINE TypeHandle _get_type_handle(const long * const *) { return long_p_type_handle; diff --git a/dtool/src/dtoolutil/vector_uchar.cxx b/dtool/src/dtoolutil/vector_uchar.cxx index 78750619bf..2087d20587 100644 --- a/dtool/src/dtoolutil/vector_uchar.cxx +++ b/dtool/src/dtoolutil/vector_uchar.cxx @@ -19,3 +19,25 @@ #define NAME vector_uchar #include "vector_src.cxx" + +/** + * Writes a hex representation of the bytes to the output stream. + */ +std::ostream & +operator << (std::ostream &out, const vector_uchar &data) { + static const char nibble_to_hex[] = "0123456789abcdef"; + + if (data.empty()) { + return out; + } + + out << nibble_to_hex[(data[0] & 0xf0) >> 4]; + out << nibble_to_hex[data[0] & 0xf]; + + for (size_t i = 1; i < data.size(); ++i) { + out << ' '; + out << nibble_to_hex[(data[i] & 0xf0) >> 4]; + out << nibble_to_hex[data[i] & 0xf]; + } + return out; +} diff --git a/dtool/src/dtoolutil/vector_uchar.h b/dtool/src/dtoolutil/vector_uchar.h index 3700b4ba8d..e04fd89289 100644 --- a/dtool/src/dtoolutil/vector_uchar.h +++ b/dtool/src/dtoolutil/vector_uchar.h @@ -30,4 +30,7 @@ #include "vector_src.h" +EXPCL_DTOOL_DTOOLUTIL std::ostream & +operator << (std::ostream &out, const vector_uchar &data); + #endif diff --git a/panda/src/event/eventParameter.I b/panda/src/event/eventParameter.I index 1d3d168344..61cbacb0b8 100644 --- a/panda/src/event/eventParameter.I +++ b/panda/src/event/eventParameter.I @@ -63,6 +63,11 @@ EventParameter(const std::string &value) : _ptr(new EventStoreString(value)) { } INLINE EventParameter:: EventParameter(const std::wstring &value) : _ptr(new EventStoreWstring(value)) { } +/** + * Defines an EventParameter that stores a bytes value. + */ +INLINE EventParameter:: +EventParameter(const vector_uchar &value) : _ptr(new ParamBytes(value)) { } /** * @@ -185,6 +190,28 @@ get_wstring_value() const { return ((const EventStoreWstring *)_ptr.p())->get_value(); } +/** + * Returns true if the EventParameter stores a binary bytes value, false + * otherwise. + */ +INLINE bool EventParameter:: +is_bytes() const { + if (is_empty()) { + return false; + } + return _ptr->is_of_type(ParamBytes::get_class_type()); +} + +/** + * Retrieves the value stored in the EventParameter. It is only valid to call + * this if is_bytes() has already returned true. + */ +INLINE vector_uchar EventParameter:: +get_bytes_value() const { + nassertr(is_bytes(), vector_uchar()); + return ((const ParamBytes *)_ptr.p())->get_value(); +} + /** * Returns true if the EventParameter stores a TypedReferenceCount pointer, * false otherwise. Note that a TypedReferenceCount is not exactly the same diff --git a/panda/src/event/eventParameter.h b/panda/src/event/eventParameter.h index 44e891e3c9..7994796b75 100644 --- a/panda/src/event/eventParameter.h +++ b/panda/src/event/eventParameter.h @@ -41,6 +41,7 @@ PUBLISHED: INLINE EventParameter(double value); INLINE EventParameter(const std::string &value); INLINE EventParameter(const std::wstring &value); + INLINE EventParameter(const vector_uchar &value); INLINE EventParameter(const EventParameter ©); INLINE EventParameter &operator = (const EventParameter ©); @@ -59,6 +60,8 @@ PUBLISHED: INLINE std::string get_string_value() const; INLINE bool is_wstring() const; INLINE std::wstring get_wstring_value() const; + INLINE bool is_bytes() const; + INLINE vector_uchar get_bytes_value() const; INLINE bool is_typed_ref_count() const; INLINE TypedReferenceCount *get_typed_ref_count_value() const; diff --git a/panda/src/putil/config_putil.cxx b/panda/src/putil/config_putil.cxx index 9849cd5b84..c9e8e4b9b1 100644 --- a/panda/src/putil/config_putil.cxx +++ b/panda/src/putil/config_putil.cxx @@ -212,6 +212,7 @@ init_libputil() { FactoryParam::init_type(); Namable::init_type(); NodeCachedReferenceCount::init_type(); + ParamBytes::init_type("ParamBytes"); ParamMatrix3d::init_type("ParamMatrix3d"); ParamMatrix3f::init_type("ParamMatrix3f"); ParamMatrix4d::init_type("ParamMatrix4d"); diff --git a/panda/src/putil/paramValue.cxx b/panda/src/putil/paramValue.cxx index 5ffcf2807c..88add8a9c0 100644 --- a/panda/src/putil/paramValue.cxx +++ b/panda/src/putil/paramValue.cxx @@ -16,6 +16,7 @@ template class ParamValue; template class ParamValue; +template class ParamValue; template class ParamValue; template class ParamValue; diff --git a/panda/src/putil/paramValue.h b/panda/src/putil/paramValue.h index f7e4f207a1..f98e76d3f1 100644 --- a/panda/src/putil/paramValue.h +++ b/panda/src/putil/paramValue.h @@ -22,6 +22,7 @@ #include "bamReader.h" #include "bamWriter.h" #include "luse.h" +#include "vector_uchar.h" /** * A non-template base class of ParamValue (below), which serves mainly to @@ -152,6 +153,7 @@ private: EXPORT_TEMPLATE_CLASS(EXPCL_PANDA_PUTIL, EXPTP_PANDA_PUTIL, ParamValue); EXPORT_TEMPLATE_CLASS(EXPCL_PANDA_PUTIL, EXPTP_PANDA_PUTIL, ParamValue); +EXPORT_TEMPLATE_CLASS(EXPCL_PANDA_PUTIL, EXPTP_PANDA_PUTIL, ParamValue); EXPORT_TEMPLATE_CLASS(EXPCL_PANDA_PUTIL, EXPTP_PANDA_PUTIL, ParamValue); EXPORT_TEMPLATE_CLASS(EXPCL_PANDA_PUTIL, EXPTP_PANDA_PUTIL, ParamValue); @@ -173,6 +175,7 @@ EXPORT_TEMPLATE_CLASS(EXPCL_PANDA_PUTIL, EXPTP_PANDA_PUTIL, ParamValue ParamString; typedef ParamValue ParamWstring; +typedef ParamValue ParamBytes; typedef ParamValue ParamVecBase2d; typedef ParamValue ParamVecBase2f; From 136e1279eaf13a2756648818e10f49b8b3621bdd Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 1 Jan 2026 01:00:19 +0100 Subject: [PATCH 254/291] Implement ShaderBuffer async extract_data method Fixes #1173 --- panda/src/display/graphicsStateGuardian.cxx | 10 ++++ panda/src/display/graphicsStateGuardian.h | 3 ++ .../glstuff/glGraphicsStateGuardian_src.cxx | 54 +++++++++++++++++++ .../src/glstuff/glGraphicsStateGuardian_src.h | 3 ++ panda/src/gobj/preparedGraphicsObjects.cxx | 15 ++++++ panda/src/gobj/preparedGraphicsObjects.h | 11 ++++ panda/src/gobj/shaderBuffer.cxx | 20 +++++++ panda/src/gobj/shaderBuffer.h | 4 ++ panda/src/gsgbase/graphicsStateGuardianBase.h | 5 ++ 9 files changed, 125 insertions(+) diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 11ec2540dd..394aa39731 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -792,6 +792,16 @@ extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, return false; } +/** + * Asynchronous version of extract_shader_buffer_data. It is the caller's + * responsibility that the data argument outlasts the token. + */ +void GraphicsStateGuardian:: +async_extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, + size_t start, size_t size, CompletionToken token) { + token.complete(extract_shader_buffer_data(buffer, data, start, size)); +} + /** * Begins a new occlusion query. After this call, you may call * begin_draw_primitives() and draw_triangles()/draw_whatever() repeatedly. diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index 36ffcb0960..8d62382f18 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -330,6 +330,9 @@ public: size_t size, const unsigned char *data); virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, size_t start = 0, size_t size = (size_t)-1); + virtual void async_extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, + size_t start = 0, size_t size = (size_t)-1, + CompletionToken token = CompletionToken()); virtual void begin_occlusion_query(); virtual PT(OcclusionQueryContext) end_occlusion_query(); diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 61ec11513d..3c72075525 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -7843,6 +7843,60 @@ extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, } #endif +#ifndef OPENGLES +/** + * Asynchronous version of extract_shader_buffer_data. It is the caller's + * responsibility that the data argument outlasts the token. + */ +void CLP(GraphicsStateGuardian):: +async_extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, + size_t start, size_t size, CompletionToken token) { + BufferContext *bc = buffer->prepare_now(get_prepared_objects(), this); + if (bc == nullptr || !bc->is_of_type(CLP(BufferContext)::get_class_type())) { + token.complete(false); + return; + } + + CLP(BufferContext) *gbc = DCAST(CLP(BufferContext), bc); + + size_t total_size = buffer->get_data_size_bytes(); + if (start >= total_size) { + data.clear(); + token.complete(true); + return; + } + + size = std::min(total_size - start, size); + + GLuint staging_buffer = 0; + void *mapped_ptr = nullptr; + + bind_new_client_buffer(staging_buffer, mapped_ptr, GL_COPY_WRITE_BUFFER, size); + if (_glMemoryBarrier != nullptr) { + _glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); + } + + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); + _glCopyBufferSubData(GL_SHADER_STORAGE_BUFFER, GL_COPY_WRITE_BUFFER, start, 0, size); + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + _glBindBuffer(GL_COPY_READ_BUFFER, 0); + _current_sbuffer_index = 0; + + if (_supports_buffer_storage && _glMemoryBarrier != nullptr) { + _glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT); + } + + insert_fence([=, &data, token = std::move(token)] (bool success) mutable { + if (success) { + data = vector_uchar((const unsigned char *)mapped_ptr, (const unsigned char *)mapped_ptr + size); + token.complete(true); + } else { + token.complete(false); + } + }); +} +#endif + #ifndef OPENGLES /** * Begins a new occlusion query. After this call, you may call diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index bd0eff59d3..eb5233e701 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -403,6 +403,9 @@ public: size_t size, const unsigned char *data); virtual bool extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, size_t start, size_t size); + virtual void async_extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, + size_t start = 0, size_t size = (size_t)-1, + CompletionToken token = CompletionToken()); #endif #ifndef OPENGLES diff --git a/panda/src/gobj/preparedGraphicsObjects.cxx b/panda/src/gobj/preparedGraphicsObjects.cxx index 3089537dd2..927c5f2111 100644 --- a/panda/src/gobj/preparedGraphicsObjects.cxx +++ b/panda/src/gobj/preparedGraphicsObjects.cxx @@ -1380,6 +1380,15 @@ prepare_shader_buffer_now(ShaderBuffer *data, GraphicsStateGuardianBase *gsg) { return bc; } +/** + * Schedules the given function to be called during the next begin_frame(). + */ +void PreparedGraphicsObjects:: +enqueue_call(EnqueuedCall &&call) { + ReMutexHolder holder(_lock); + _enqueued_calls.push_back(std::move(call)); +} + /** * Creates a new future for the given object. */ @@ -1601,6 +1610,12 @@ begin_frame(GraphicsStateGuardianBase *gsg, Thread *current_thread) { } _enqueued_shader_buffers.clear(); + + for (EnqueuedCall &call : _enqueued_calls) { + std::move(call)(gsg); + } + + _enqueued_calls.clear(); } /** diff --git a/panda/src/gobj/preparedGraphicsObjects.h b/panda/src/gobj/preparedGraphicsObjects.h index fac1ab630d..f47d3a9275 100644 --- a/panda/src/gobj/preparedGraphicsObjects.h +++ b/panda/src/gobj/preparedGraphicsObjects.h @@ -31,6 +31,10 @@ #include "adaptiveLru.h" #include "asyncFuture.h" +#ifndef CPPPARSER +#include +#endif + class TextureContext; class SamplerContext; class GeomContext; @@ -160,6 +164,12 @@ PUBLISHED: GraphicsStateGuardianBase *gsg); public: +#ifndef CPPPARSER + typedef std::function EnqueuedCall; + + void enqueue_call(EnqueuedCall &&call); +#endif + /** * This is a handle to an enqueued object, from which the result can be * obtained upon completion. @@ -281,6 +291,7 @@ private: Buffers _prepared_shader_buffers; pvector _released_shader_buffers; EnqueuedShaderBuffers _enqueued_shader_buffers; + pvector _enqueued_calls; BufferCache _vertex_buffer_cache; BufferCacheLRU _vertex_buffer_cache_lru; diff --git a/panda/src/gobj/shaderBuffer.cxx b/panda/src/gobj/shaderBuffer.cxx index 9164a870ae..8872b64287 100644 --- a/panda/src/gobj/shaderBuffer.cxx +++ b/panda/src/gobj/shaderBuffer.cxx @@ -32,6 +32,26 @@ output(std::ostream &out) const { out << "buffer " << get_name() << ", " << _data_size_bytes << "B, " << _usage_hint; } +/** + * Asynchronously requests to read the given subset of the data on the CPU. + */ +PT(AsyncFuture) ShaderBuffer:: +extract_data(PreparedGraphicsObjects *prepared_objects, size_t start, size_t size) { + PT(AsyncFuture) future = new AsyncFuture; + PT(ParamBytes) bytes = new ParamBytes(vector_uchar()); + + prepared_objects->enqueue_call([=, bytes = std::move(bytes)] (GraphicsStateGuardianBase *gsg) { + gsg->async_extract_shader_buffer_data(this, (vector_uchar &)bytes->get_value(), start, size, [future = std::move(future), bytes = std::move(bytes)] (bool success) { + if (success) { + future->set_result(bytes); + } else { + future->set_result(nullptr); + } + }); + }); + return future; +} + /** * Indicates that the data should be enqueued to be prepared in the indicated * prepared_objects at the beginning of the next frame. This will ensure the diff --git a/panda/src/gobj/shaderBuffer.h b/panda/src/gobj/shaderBuffer.h index d523e13256..8cc9eedf90 100644 --- a/panda/src/gobj/shaderBuffer.h +++ b/panda/src/gobj/shaderBuffer.h @@ -21,6 +21,7 @@ #include "graphicsStateGuardianBase.h" #include "factoryParams.h" #include "vector_uchar.h" +#include "asyncFuture.h" class BufferContext; class PreparedGraphicsObjects; @@ -51,6 +52,9 @@ PUBLISHED: MAKE_PROPERTY(data_size_bytes, get_data_size_bytes); MAKE_PROPERTY(usage_hint, get_usage_hint); + PT(AsyncFuture) extract_data(PreparedGraphicsObjects *prepared_objects, + size_t start = 0, size_t size = (size_t)-1); + void prepare(PreparedGraphicsObjects *prepared_objects); bool is_prepared(PreparedGraphicsObjects *prepared_objects) const; diff --git a/panda/src/gsgbase/graphicsStateGuardianBase.h b/panda/src/gsgbase/graphicsStateGuardianBase.h index d49f144046..c3fac9a003 100644 --- a/panda/src/gsgbase/graphicsStateGuardianBase.h +++ b/panda/src/gsgbase/graphicsStateGuardianBase.h @@ -23,6 +23,7 @@ #include "patomic.h" #include "small_vector.h" #include "completionToken.h" +#include "vector_uchar.h" // A handful of forward references. @@ -32,6 +33,7 @@ class GraphicsWindow; class NodePath; class GraphicsOutputBase; class ScreenshotRequest; +class AsyncFuture; class VertexBufferContext; class IndexBufferContext; @@ -175,6 +177,9 @@ public: virtual BufferContext *prepare_shader_buffer(ShaderBuffer *data)=0; virtual void release_shader_buffer(BufferContext *ibc)=0; virtual void release_shader_buffers(const pvector &contexts)=0; + virtual void async_extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, + size_t start = 0, size_t size = (size_t)-1, + CompletionToken token = CompletionToken())=0; virtual void dispatch_compute(int size_x, int size_y, int size_z)=0; From 3c852c8d7238a29b71a00a078538268ec6d83284 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 2 Jan 2026 09:50:04 +0100 Subject: [PATCH 255/291] audio: Disable OpenAL hrtf by default, fixes macOS init crash --- .github/workflows/ci.yml | 2 +- panda/src/audio/config_audio.cxx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c86b5b0053..69558d097f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -396,7 +396,7 @@ jobs: tar -xf panda3d-1.10.16-tools-mac.tar.gz mv panda3d-1.10.16/thirdparty thirdparty rmdir panda3d-1.10.16 - (cd thirdparty/darwin-libs-a && rm -rf rocket openal) + (cd thirdparty/darwin-libs-a && rm -rf rocket) - name: Set up XCode (macOS) if: runner.os == 'macOS' diff --git a/panda/src/audio/config_audio.cxx b/panda/src/audio/config_audio.cxx index de128896e6..5f2708cfae 100644 --- a/panda/src/audio/config_audio.cxx +++ b/panda/src/audio/config_audio.cxx @@ -76,7 +76,7 @@ ConfigVariableInt audio_preload_threshold "time - the hard drive seek time makes it stutter.")); ConfigVariableBool audio_want_hrtf -("audio-want-hrtf", true, +("audio-want-hrtf", false, PRC_DESC("This determines whether OpenAL-Soft should activate HRTF in " "certain hardware configurations. Set it to true to cause " "OpenAL to automatically apply HRTF processing to all output " From f958fdced32007f1ec52f4403b890459478b6639 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 7 Jan 2026 14:07:38 +0100 Subject: [PATCH 256/291] interval: fix Python callable inputs not called in ET_instant mode Fixes #1799 --- direct/src/interval/LerpInterval.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/direct/src/interval/LerpInterval.py b/direct/src/interval/LerpInterval.py index e303b3a5d1..3058395d22 100644 --- a/direct/src/interval/LerpInterval.py +++ b/direct/src/interval/LerpInterval.py @@ -122,7 +122,7 @@ class LerpPosInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndPos, self.endPos) self.setupParam(self.setStartPos, self.startPos) LerpNodePathInterval.privDoEvent(self, t, event) @@ -153,7 +153,7 @@ class LerpHprInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndHpr, self.endHpr) self.setupParam(self.setStartHpr, self.startHpr) self.setupParam(self.setStartQuat, self.startQuat) @@ -192,7 +192,7 @@ class LerpQuatInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndQuat, self.endQuat) self.setupParam(self.setStartHpr, self.startHpr) self.setupParam(self.setStartQuat, self.startQuat) @@ -219,7 +219,7 @@ class LerpScaleInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndScale, self.endScale) self.setupParam(self.setStartScale, self.startScale) LerpNodePathInterval.privDoEvent(self, t, event) @@ -245,7 +245,7 @@ class LerpShearInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndShear, self.endShear) self.setupParam(self.setStartShear, self.startShear) LerpNodePathInterval.privDoEvent(self, t, event) @@ -280,7 +280,7 @@ class LerpPosHprInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndPos, self.endPos) self.setupParam(self.setStartPos, self.startPos) self.setupParam(self.setEndHpr, self.endHpr) @@ -326,7 +326,7 @@ class LerpPosQuatInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndPos, self.endPos) self.setupParam(self.setStartPos, self.startPos) self.setupParam(self.setEndQuat, self.endQuat) @@ -365,7 +365,7 @@ class LerpHprScaleInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndHpr, self.endHpr) self.setupParam(self.setStartHpr, self.startHpr) self.setupParam(self.setStartQuat, self.startQuat) @@ -414,7 +414,7 @@ class LerpQuatScaleInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndQuat, self.endQuat) self.setupParam(self.setStartHpr, self.startHpr) self.setupParam(self.setStartQuat, self.startQuat) @@ -459,7 +459,7 @@ class LerpPosHprScaleInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndPos, self.endPos) self.setupParam(self.setStartPos, self.startPos) self.setupParam(self.setEndHpr, self.endHpr) @@ -516,7 +516,7 @@ class LerpPosQuatScaleInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndPos, self.endPos) self.setupParam(self.setStartPos, self.startPos) self.setupParam(self.setEndQuat, self.endQuat) @@ -569,7 +569,7 @@ class LerpPosHprScaleShearInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndPos, self.endPos) self.setupParam(self.setStartPos, self.startPos) self.setupParam(self.setEndHpr, self.endHpr) @@ -636,7 +636,7 @@ class LerpPosQuatScaleShearInterval(LerpNodePathInterval): def privDoEvent(self, t, event): # This function is only used if Python functors were passed in # for some of the input parameters. - if self.paramSetup and event == CInterval.ETInitialize: + if self.paramSetup and (event == CInterval.ETInitialize or event == CInterval.ETInstant): self.setupParam(self.setEndPos, self.endPos) self.setupParam(self.setStartPos, self.startPos) self.setupParam(self.setEndQuat, self.endQuat) From 0bb8a4519a45ee8d0d92bd0f70aaa4af90c47d22 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 7 Jan 2026 19:34:49 +0100 Subject: [PATCH 257/291] Use PyObject_CallOneArg instead of PyObject_CallFunctionObjArgs for perf --- dtool/src/dtoolutil/filename_ext.cxx | 2 +- panda/src/event/pythonTask.cxx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dtool/src/dtoolutil/filename_ext.cxx b/dtool/src/dtoolutil/filename_ext.cxx index 12b85111d0..4a040708b1 100644 --- a/dtool/src/dtoolutil/filename_ext.cxx +++ b/dtool/src/dtoolutil/filename_ext.cxx @@ -67,7 +67,7 @@ __init__(PyObject *path) { return; } - PyObject *path_str = PyObject_CallFunctionObjArgs(fspath, path, nullptr); + PyObject *path_str = PyObject_CallOneArg(fspath, path); Py_DECREF(fspath); if (path_str == nullptr) { return; diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index 57c3bd88cc..bc540a75ac 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -652,7 +652,7 @@ do_python_task() { PyObject *func = PyObject_GetAttrString(_generator, "throw"); if (func != nullptr) { PyObject *exc = PyObject_CallNoArgs(exc_type); - result = PyObject_CallFunctionObjArgs(func, exc, nullptr); + result = PyObject_CallOneArg(func, exc); Py_DECREF(exc); Py_DECREF(func); } else { From 80806510c29024df2d2de72a600dd6ea6fe9820a Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 7 Jan 2026 19:35:35 +0100 Subject: [PATCH 258/291] express: Make PTA init from Python sequence faster by using vectorcall --- panda/src/express/pointerToArray_ext.I | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/panda/src/express/pointerToArray_ext.I b/panda/src/express/pointerToArray_ext.I index 849f993cba..74188b9497 100644 --- a/panda/src/express/pointerToArray_ext.I +++ b/panda/src/express/pointerToArray_ext.I @@ -106,6 +106,9 @@ __init__(PyObject *self, PyObject *source) { // We need to initialize the this pointer before we can call push_back. DtoolInstance_INIT_PTR(self, this->_this); + PyObject *args[3]; + args[1] = self; + Py_BEGIN_CRITICAL_SECTION(source); Py_ssize_t size = PySequence_Size(source); this->_this->reserve(size); @@ -114,7 +117,8 @@ __init__(PyObject *self, PyObject *source) { if (item == nullptr) { break; } - PyObject *result = PyObject_CallFunctionObjArgs(push_back, self, item, nullptr); + args[2] = item; + PyObject *result = _PyObject_Vectorcall(push_back, args + 1, 2 | PY_VECTORCALL_ARGUMENTS_OFFSET, nullptr); Py_DECREF(item); if (result == nullptr) { // Unable to add item--probably it wasn't of the appropriate type. From b630e14dc4f2c3ef7594f864eb29cb61578af8a0 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 7 Jan 2026 19:50:08 +0100 Subject: [PATCH 259/291] py_compat: Add vectorcall functions --- dtool/src/interrogatedb/py_compat.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dtool/src/interrogatedb/py_compat.h b/dtool/src/interrogatedb/py_compat.h index efbdc74d83..bfcea61e76 100644 --- a/dtool/src/interrogatedb/py_compat.h +++ b/dtool/src/interrogatedb/py_compat.h @@ -219,6 +219,14 @@ INLINE PyObject *_PyLong_Lshift(PyObject *a, size_t shiftby) { } #endif +#ifndef PY_VECTORCALL_ARGUMENTS_OFFSET +# define PY_VECTORCALL_ARGUMENTS_OFFSET (((size_t)1) << (8 * sizeof(size_t) - 1)) +#endif + +#if PY_VERSION_HEX < 0x030800B1 +# define PyVectorcall_NARGS(n) ((n) & ~PY_VECTORCALL_ARGUMENTS_OFFSET) +#endif + /* Python 3.9 */ #if PY_VERSION_HEX < 0x030900A4 && !defined(Py_IS_TYPE) @@ -229,6 +237,10 @@ INLINE PyObject *_PyLong_Lshift(PyObject *a, size_t shiftby) { # define PyCFunction_CheckExact(op) (Py_IS_TYPE(op, &PyCFunction_Type)) #endif +#if PY_VERSION_HEX < 0x030900A4 +# define PyObject_Vectorcall(callable, args, nargsf, kwnames) (_PyObject_Vectorcall((callable), (args), (nargsf), (kwnames))) +#endif + #if PY_VERSION_HEX < 0x03090000 INLINE PyObject *PyObject_CallNoArgs(PyObject *func) { #if PY_VERSION_HEX >= 0x03080000 From b3ac6e789676ae8e6871755ea9435bfce925931a Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 7 Jan 2026 19:51:54 +0100 Subject: [PATCH 260/291] gobj: Fix warnings due to GCC being terrible at static analysis --- panda/src/gobj/adaptiveLru.I | 5 ++++- panda/src/gobj/geom.cxx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/panda/src/gobj/adaptiveLru.I b/panda/src/gobj/adaptiveLru.I index 41acf47c38..ae3a62c3f2 100644 --- a/panda/src/gobj/adaptiveLru.I +++ b/panda/src/gobj/adaptiveLru.I @@ -134,7 +134,10 @@ get_lru() const { */ INLINE void AdaptiveLruPage:: dequeue_lru() { - enqueue_lru(nullptr); + if (_lru != nullptr) { + _lru->do_remove_page(this); + _lru = nullptr; + } } /** diff --git a/panda/src/gobj/geom.cxx b/panda/src/gobj/geom.cxx index 8a9ae9c35e..447abf93ee 100644 --- a/panda/src/gobj/geom.cxx +++ b/panda/src/gobj/geom.cxx @@ -1373,7 +1373,7 @@ compute_internal_bounds(Geom::CData *cdata, Thread *current_thread) const { nassertv(!pmax.is_nan()); // Then we put the bounding volume around both of those points. - PN_stdfloat avg_box_area; + PN_stdfloat avg_box_area {}; switch (btype) { case BoundingVolume::BT_best: case BoundingVolume::BT_fastest: From 075353003d2188ac91aa884af28fc113164daf03 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 7 Jan 2026 19:52:24 +0100 Subject: [PATCH 261/291] gobj: Fix some int signedness comparison warnings --- panda/src/gobj/texture.cxx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index b8b1f5bbf1..abbe304b73 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -7613,7 +7613,7 @@ get_ram_image_as(const string &requested_format) { // If the image size wasn't a multiple of 4, we may have a handful of // pixels left over. Convert those the slower way. uint8_t *tail = (uint8_t *)dst; - for (int i = (imgsize & ~0x3); i < imgsize; ++i) { + for (size_t i = (imgsize & ~0x3); i < imgsize; ++i) { uint32_t v = *src++; *tail++ = (v & 0x00ff0000u) >> 16; *tail++ = (v & 0x0000ff00u) >> 8; @@ -7627,8 +7627,8 @@ get_ram_image_as(const string &requested_format) { // Convert blocks of 4 pixels at a time, so that we can treat both the // source and destination as 32-bit integers. - int blocks = imgsize >> 2; - for (int i = 0; i < blocks; ++i) { + size_t blocks = imgsize >> 2; + for (size_t i = 0; i < blocks; ++i) { uint32_t v0 = *src++; uint32_t v1 = *src++; uint32_t v2 = *src++; @@ -7641,7 +7641,7 @@ get_ram_image_as(const string &requested_format) { // If the image size wasn't a multiple of 4, we may have a handful of // pixels left over. Convert those the slower way. uint8_t *tail = (uint8_t *)dst; - for (int i = (imgsize & ~0x3); i < imgsize; ++i) { + for (size_t i = (imgsize & ~0x3); i < imgsize; ++i) { uint32_t v = *src++; *tail++ = (v & 0x000000ffu); *tail++ = (v & 0x0000ff00u) >> 8; @@ -7653,7 +7653,7 @@ get_ram_image_as(const string &requested_format) { uint8_t *dst = (uint8_t *)newdata.p(); if (format == "RGB" && cdata->_num_components == 3) { - for (int i = 0; i < imgsize; ++i) { + for (size_t i = 0; i < imgsize; ++i) { *dst++ = src[2]; *dst++ = src[1]; *dst++ = src[0]; From 8fe8862f063da8eac4dfed5ca466c62bc9ec14d6 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 7 Jan 2026 19:59:45 +0100 Subject: [PATCH 262/291] dtoolbase: The default pvector constructor shouldn't be explicit --- dtool/src/dtoolbase/pvector.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dtool/src/dtoolbase/pvector.h b/dtool/src/dtoolbase/pvector.h index f8dcb21071..edd6cca579 100644 --- a/dtool/src/dtoolbase/pvector.h +++ b/dtool/src/dtoolbase/pvector.h @@ -46,7 +46,8 @@ public: typedef std::vector base_class; typedef typename base_class::size_type size_type; - explicit pvector(TypeHandle type_handle = get_type_handle(pvector)) : base_class(allocator(type_handle)) { } + pvector() : base_class(allocator(get_type_handle(pvector))) { } + explicit pvector(TypeHandle type_handle) : base_class(allocator(type_handle)) { } pvector(const pvector ©) : base_class(copy) { } pvector(pvector &&from) noexcept : base_class(std::move(from)) {}; explicit pvector(size_type n, TypeHandle type_handle = get_type_handle(pvector)) : base_class(n, Type(), allocator(type_handle)) { } From 5a82ac320831e7b5c5699452b9f0063f48ecc8c8 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 7 Jan 2026 20:09:20 +0100 Subject: [PATCH 263/291] Use PyObject_Vectorcall for calls to Python for better efficiency No longer requires creating temporary tuple to hold the args. Introduced in Python 3.8. --- panda/src/event/asyncFuture_ext.cxx | 3 +- panda/src/event/pythonTask.cxx | 90 +++++++++++++--------- panda/src/event/pythonTask.h | 2 +- panda/src/gobj/pythonTexturePoolFilter.cxx | 32 ++++---- panda/src/pgraph/pythonLoaderFileType.cxx | 31 ++++---- panda/src/pipeline/pythonThread.cxx | 12 +-- panda/src/pipeline/pythonThread.h | 3 +- panda/src/putil/bamReader_ext.cxx | 13 ++-- panda/src/putil/pythonCallbackObject.cxx | 15 ++-- 9 files changed, 112 insertions(+), 89 deletions(-) diff --git a/panda/src/event/asyncFuture_ext.cxx b/panda/src/event/asyncFuture_ext.cxx index 0ac681f1b2..c181085280 100644 --- a/panda/src/event/asyncFuture_ext.cxx +++ b/panda/src/event/asyncFuture_ext.cxx @@ -332,8 +332,7 @@ add_done_callback(PyObject *self, PyObject *fn) { } PythonTask *task = new PythonTask(fn); - Py_DECREF(task->_args); - task->_args = PyTuple_Pack(1, self); + task->_args.assign(1, Py_NewRef(self)); task->_append_task = false; task->_ignore_return = true; diff --git a/panda/src/event/pythonTask.cxx b/panda/src/event/pythonTask.cxx index bc540a75ac..264442d27e 100644 --- a/panda/src/event/pythonTask.cxx +++ b/panda/src/event/pythonTask.cxx @@ -37,7 +37,6 @@ PythonTask:: PythonTask(PyObject *func_or_coro, const std::string &name) : AsyncTask(name), _function(nullptr), - _args(nullptr), _upon_death(nullptr), _owner(nullptr), _exception(nullptr), @@ -45,6 +44,7 @@ PythonTask(PyObject *func_or_coro, const std::string &name) : _exc_traceback(nullptr), _generator(nullptr), _fut_waiter(nullptr), + _append_task(true), _ignore_return(false), _registered_to_owner(false), _retrieved_exception(false) { @@ -63,7 +63,6 @@ PythonTask(PyObject *func_or_coro, const std::string &name) : nassert_raise("Invalid function passed to PythonTask"); } - set_args(Py_None, true); set_upon_death(Py_None); set_owner(Py_None); @@ -104,7 +103,9 @@ PythonTask:: // All of these may have already been cleared by __clear__. Py_XDECREF(_function); - Py_XDECREF(_args); + for (PyObject *arg : _args) { + Py_DECREF(arg); + } Py_XDECREF(__dict__); Py_XDECREF(_exception); Py_XDECREF(_exc_value); @@ -134,21 +135,23 @@ set_function(PyObject *function) { */ void PythonTask:: set_args(PyObject *args, bool append_task) { - Py_XDECREF(_args); - _args = nullptr; + pvector old_args = std::exchange(_args, {}); - if (args == Py_None) { - // None means no arguments; create an empty tuple. - _args = PyTuple_New(0); - } else { - if (PySequence_Check(args)) { - _args = PySequence_Tuple(args); + if (PySequence_Check(args)) { + Py_ssize_t len = PySequence_Size(args); + _args.resize(len); + + for (Py_ssize_t i = 0; i < len; ++i) { + _args[i] = PySequence_GetItem(args, i); } } - - if (_args == nullptr) { + else if (args != Py_None) { nassert_raise("Invalid args passed to PythonTask"); - _args = PyTuple_New(0); + return; + } + + for (PyObject *old_arg : old_args) { + Py_DECREF(old_arg); } _append_task = append_task; @@ -159,19 +162,18 @@ set_args(PyObject *args, bool append_task) { */ PyObject *PythonTask:: get_args() { + // If we want to append the task, we have to create a new tuple with space + // for one more at the end. We have to do this dynamically each time, to + // avoid storing the task itself in its own arguments list, and thereby + // creating a cyclical reference. + + size_t num_args = _args.size(); + PyObject *result = PyTuple_New(num_args + _append_task); + for (size_t i = 0; i < num_args; ++i) { + PyTuple_SET_ITEM(result, i, Py_NewRef(_args[i])); + } + if (_append_task) { - // If we want to append the task, we have to create a new tuple with space - // for one more at the end. We have to do this dynamically each time, to - // avoid storing the task itself in its own arguments list, and thereby - // creating a cyclical reference. - - int num_args = PyTuple_GET_SIZE(_args); - PyObject *with_task = PyTuple_New(num_args + 1); - for (int i = 0; i < num_args; ++i) { - PyObject *item = PyTuple_GET_ITEM(_args, i); - PyTuple_SET_ITEM(with_task, i, Py_NewRef(item)); - } - // Check whether we have a Python wrapper. This is not the case if the // object has been created by C++ and never been exposed to Python code. if (__self__ == nullptr) { @@ -180,12 +182,9 @@ get_args() { __self__ = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false); } - PyTuple_SET_ITEM(with_task, num_args, Py_NewRef(__self__)); - return with_task; - } - else { - return Py_NewRef(_args); + PyTuple_SET_ITEM(result, num_args, Py_NewRef(__self__)); } + return result; } /** @@ -376,7 +375,9 @@ int PythonTask:: __traverse__(visitproc visit, void *arg) { Py_VISIT(__self__); Py_VISIT(_function); - Py_VISIT(_args); + for (PyObject *arg : _args) { + Py_VISIT(arg); + } Py_VISIT(_upon_death); Py_VISIT(_owner); Py_VISIT(__dict__); @@ -390,7 +391,12 @@ __traverse__(visitproc visit, void *arg) { int PythonTask:: __clear__() { Py_CLEAR(_function); - Py_CLEAR(_args); + { + pvector old_args = std::exchange(_args, {}); + for (PyObject *arg : old_args) { + Py_DECREF(arg); + } + } Py_CLEAR(_upon_death); Py_CLEAR(_owner); Py_CLEAR(__dict__); @@ -590,9 +596,21 @@ do_python_task() { // We are calling the function directly. nassertr(_function != nullptr, DS_interrupt); - PyObject *args = get_args(); - result = PythonThread::call_python_func(_function, args); - Py_DECREF(args); + size_t nargs = _args.size(); + PyObject **args = (PyObject **)alloca(sizeof(PyObject *) * (nargs + 2)) + 1; + std::copy(_args.begin(), _args.end(), args); + + if (_append_task) { + // Check whether we have a Python wrapper. This is not the case if the + // object has been created by C++ and never been exposed to Python code. + if (__self__ == nullptr) { + // A __self__ instance does not exist, let's create one now. + ref(); + __self__ = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false); + } + args[nargs++] = __self__; + } + result = PythonThread::call_python_func(_function, args, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET); if (result != nullptr && PyGen_Check(result)) { // The function has yielded a generator. We will call into that diff --git a/panda/src/event/pythonTask.h b/panda/src/event/pythonTask.h index 148252e999..67ae3ed17a 100644 --- a/panda/src/event/pythonTask.h +++ b/panda/src/event/pythonTask.h @@ -114,7 +114,7 @@ private: private: PyObject *_function; - PyObject *_args; + pvector _args; PyObject *_upon_death; PyObject *_owner; diff --git a/panda/src/gobj/pythonTexturePoolFilter.cxx b/panda/src/gobj/pythonTexturePoolFilter.cxx index 0fc11b181d..f20cc094ce 100644 --- a/panda/src/gobj/pythonTexturePoolFilter.cxx +++ b/panda/src/gobj/pythonTexturePoolFilter.cxx @@ -101,17 +101,19 @@ pre_load(const Filename &orig_filename, const Filename &orig_alpha_filename, #endif // Wrap the arguments. - PyObject *args = Py_BuildValue("(OOiiNO)", - DTool_CreatePyInstance((void *)&orig_filename, Dtool_Filename, false, true), - DTool_CreatePyInstance((void *)&orig_alpha_filename, Dtool_Filename, false, true), - primary_file_num_channels, - alpha_file_channel, - PyBool_FromLong(read_mipmaps), - DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true) - ); + PyObject *args[7]; + args[1] = DTool_CreatePyInstance((void *)&orig_filename, Dtool_Filename, false, true); + args[2] = DTool_CreatePyInstance((void *)&orig_alpha_filename, Dtool_Filename, false, true); + args[3] = PyLong_FromLong(primary_file_num_channels); + args[4] = PyLong_FromLong(alpha_file_channel); + args[5] = PyBool_FromLong(read_mipmaps); + args[6] = DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true); - PyObject *result = PythonThread::call_python_func(_pre_load_func, args); - Py_DECREF(args); + PyObject *result = PythonThread::call_python_func(_pre_load_func, args + 1, 6 | PY_VECTORCALL_ARGUMENTS_OFFSET); + + for (size_t i = 1; i < 7; ++i) { + Py_DECREF(args[i]); + } PT(Texture) tex; if (result != nullptr) { @@ -162,11 +164,11 @@ post_load(Texture *tex) { #endif // Wrap the arguments. - PyObject *args = PyTuple_Pack(1, - DTool_CreatePyInstance(tex, Dtool_Texture, true, false) - ); - PyObject *result = PythonThread::call_python_func(_post_load_func, args); - Py_DECREF(args); + PyObject *args[2]; + args[1] = DTool_CreatePyInstance(tex, Dtool_Texture, true, false); + + PyObject *result = PythonThread::call_python_func(_post_load_func, args + 1, 1 | PY_VECTORCALL_ARGUMENTS_OFFSET); + Py_DECREF(args[1]); PT(Texture) result_tex; if (result != nullptr) { diff --git a/panda/src/pgraph/pythonLoaderFileType.cxx b/panda/src/pgraph/pythonLoaderFileType.cxx index aa4b84cceb..b33930cffe 100644 --- a/panda/src/pgraph/pythonLoaderFileType.cxx +++ b/panda/src/pgraph/pythonLoaderFileType.cxx @@ -305,19 +305,19 @@ load_file(const Filename &path, const LoaderOptions &options, #endif // Wrap the arguments. - PyObject *args = PyTuple_New(3); - PyTuple_SET_ITEM(args, 0, DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true)); - PyTuple_SET_ITEM(args, 1, DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true)); + PyObject *args[4]; + args[1] = DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true); + args[2] = DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true); if (record != nullptr) { record->ref(); - PyTuple_SET_ITEM(args, 2, DTool_CreatePyInstanceTyped((void *)record, Dtool_BamCacheRecord, true, false, record->get_type_index())); + args[3] = DTool_CreatePyInstanceTyped((void *)record, Dtool_BamCacheRecord, true, false, record->get_type_index()); } else { - PyTuple_SET_ITEM(args, 2, Py_NewRef(Py_None)); + args[3] = Py_NewRef(Py_None); } PT(PandaNode) node; - PyObject *result = PythonThread::call_python_func(_load_func, args); + PyObject *result = PythonThread::call_python_func(_load_func, args + 1, 3 | PY_VECTORCALL_ARGUMENTS_OFFSET); if (result != nullptr) { if (DtoolInstance_Check(result)) { node = (PandaNode *)DtoolInstance_UPCAST(result, Dtool_PandaNode); @@ -325,7 +325,9 @@ load_file(const Filename &path, const LoaderOptions &options, Py_DECREF(result); } - Py_DECREF(args); + Py_DECREF(args[1]); + Py_DECREF(args[2]); + Py_DECREF(args[3]); if (node == nullptr) { PyObject *exc_type = PyErr_Occurred(); @@ -372,13 +374,16 @@ save_file(const Filename &path, const LoaderOptions &options, #endif // Wrap the arguments. - PyObject *args = PyTuple_New(3); - PyTuple_SET_ITEM(args, 0, DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true)); - PyTuple_SET_ITEM(args, 1, DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true)); - PyTuple_SET_ITEM(args, 2, DTool_CreatePyInstanceTyped((void *)node, Dtool_PandaNode, true, false, node->get_type_index())); + PyObject *args[4]; + args[1] = DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true); + args[2] = DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true); + args[3] = DTool_CreatePyInstanceTyped((void *)node, Dtool_PandaNode, true, false, node->get_type_index()); + + PyObject *result = PythonThread::call_python_func(_save_func, args + 1, 3 | PY_VECTORCALL_ARGUMENTS_OFFSET); + Py_DECREF(args[1]); + Py_DECREF(args[2]); + Py_DECREF(args[3]); - PyObject *result = PythonThread::call_python_func(_load_func, args); - Py_DECREF(args); if (result != nullptr) { Py_DECREF(result); } else { diff --git a/panda/src/pipeline/pythonThread.cxx b/panda/src/pipeline/pythonThread.cxx index 84770c258e..eff5728aaf 100644 --- a/panda/src/pipeline/pythonThread.cxx +++ b/panda/src/pipeline/pythonThread.cxx @@ -123,7 +123,7 @@ set_args(PyObject *args) { * exception. */ PyObject *PythonThread:: -call_python_func(PyObject *function, PyObject *args) { +call_python_func(PyObject *function, PyObject **args, size_t nargsf) { Thread *current_thread = get_current_thread(); // Create a new Python thread state data structure, so Python can properly @@ -132,7 +132,7 @@ call_python_func(PyObject *function, PyObject *args) { if (current_thread == get_main_thread()) { // In the main thread, just call the function. - result = PyObject_Call(function, args, nullptr); + result = _PyObject_Vectorcall(function, args, nargsf, nullptr); if (result == nullptr) { if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_SystemExit)) { @@ -189,7 +189,7 @@ call_python_func(PyObject *function, PyObject *args) { PyThreadState_Swap(new_thread_state); // Call the user's function. - result = PyObject_Call(function, args, nullptr); + result = _PyObject_Vectorcall(function, args, nargsf, nullptr); if (result == nullptr && PyErr_Occurred()) { // We got an exception. Move the exception from the current thread into // the main thread, so it can be handled there. @@ -230,7 +230,7 @@ call_python_func(PyObject *function, PyObject *args) { gstate = PyGILState_Ensure(); // Call the user's function. - result = PyObject_Call(function, args, nullptr); + result = _PyObject_Vectorcall(function, args, nargsf, nullptr); if (result == nullptr && PyErr_Occurred()) { // We got an exception. Move the exception from the current thread into // the main thread, so it can be handled there. @@ -275,7 +275,9 @@ call_python_func(PyObject *function, PyObject *args) { */ void PythonThread:: thread_main() { - _result = call_python_func(_function, _args); + PyObject **args = &PyTuple_GET_ITEM(_args, 0); + Py_ssize_t nargs = PyTuple_GET_SIZE(_args); + _result = call_python_func(_function, args, (size_t)nargs); } #endif // HAVE_PYTHON diff --git a/panda/src/pipeline/pythonThread.h b/panda/src/pipeline/pythonThread.h index eb57351149..f9bf6c91e9 100644 --- a/panda/src/pipeline/pythonThread.h +++ b/panda/src/pipeline/pythonThread.h @@ -36,7 +36,8 @@ public: PyObject *get_args() const; void set_args(PyObject *); - static PyObject *call_python_func(PyObject *function, PyObject *args); + static PyObject *call_python_func(PyObject *function, PyObject **args, + size_t nargsf); PUBLISHED: MAKE_PROPERTY(args, get_args, set_args); diff --git a/panda/src/putil/bamReader_ext.cxx b/panda/src/putil/bamReader_ext.cxx index 84c5060e1e..e85606b440 100644 --- a/panda/src/putil/bamReader_ext.cxx +++ b/panda/src/putil/bamReader_ext.cxx @@ -41,15 +41,14 @@ static TypedWritable *factory_callback(const FactoryParams ¶ms){ BamReader *manager; parse_params(params, scan, manager); - PyObject *py_scan = DTool_CreatePyInstance(&scan, Dtool_DatagramIterator, false, false); - PyObject *py_manager = DTool_CreatePyInstance(manager, Dtool_BamReader, false, false); - PyObject *args = PyTuple_Pack(2, py_scan, py_manager); + PyObject *args[3]; + args[1] = DTool_CreatePyInstance(&scan, Dtool_DatagramIterator, false, false); + args[2] = DTool_CreatePyInstance(manager, Dtool_BamReader, false, false); // Now call the Python function. - PyObject *result = PythonThread::call_python_func(func, args); - Py_DECREF(args); - Py_DECREF(py_scan); - Py_DECREF(py_manager); + PyObject *result = PythonThread::call_python_func(func, args + 1, 2 | PY_VECTORCALL_ARGUMENTS_OFFSET); + Py_DECREF(args[1]); + Py_DECREF(args[2]); if (result == nullptr) { util_cat.error() diff --git a/panda/src/putil/pythonCallbackObject.cxx b/panda/src/putil/pythonCallbackObject.cxx index 4fc2da8173..8607273a6a 100644 --- a/panda/src/putil/pythonCallbackObject.cxx +++ b/panda/src/putil/pythonCallbackObject.cxx @@ -129,16 +129,13 @@ void PythonCallbackObject:: do_python_callback(CallbackData *cbdata) { nassertv(cbdata != nullptr); - // Wrap the cbdata up in a Python object, then put it in a tuple, for the - // argument list. - PyObject *pycbdata = - DTool_CreatePyInstanceTyped(cbdata, Dtool_TypedObject, - false, false, cbdata->get_type_index()); - PyObject *args = Py_BuildValue("(O)", pycbdata); - Py_DECREF(pycbdata); + // Wrap the cbdata up in a Python object. + PyObject *args[2]; + args[1] = DTool_CreatePyInstanceTyped(cbdata, Dtool_TypedObject, + false, false, cbdata->get_type_index()); - PyObject *result = PythonThread::call_python_func(_function, args); - Py_DECREF(args); + PyObject *result = PythonThread::call_python_func(_function, args + 1, 1 | PY_VECTORCALL_ARGUMENTS_OFFSET); + Py_DECREF(args[1]); if (result == nullptr) { if (PyErr_Occurred() != PyExc_SystemExit) { From 586b92a4ab124a8fc5b2a1f4a985f9fe8057ddea Mon Sep 17 00:00:00 2001 From: Yaksh Bariya Date: Mon, 12 Jan 2026 07:03:37 +0530 Subject: [PATCH 264/291] makepanda: fix for python >= 3.13 on android android is now a supported platform Python since Python 3.13. Earlier logic for figuring out android is still kept to preserve backwards compatibility with Python versions <= 3.12. Also have added a comment where we are monkeypatching sys.platform to "android" to remove it once minimum python requirement is removed Found initially during Python bump for Termux (https://github.com/termux/termux-packages/pull/27739) Also seems like Pyright too hasn't caught up with this yet. So it needs to be made aware as well --- makepanda/makepandacore.py | 5 ++++- panda/src/android/site.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index 1e32c11dd3..cfc19fb1ad 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -284,7 +284,7 @@ def GetHost(): return 'darwin' elif sys.platform.startswith('linux'): try: - # Python seems to offer no built-in way to check this. + # Python versions before 3.13 reported android as "linux" osname = subprocess.check_output(["uname", "-o"]) if osname.strip().lower() == b'android': return 'android' @@ -294,6 +294,9 @@ def GetHost(): return 'linux' elif sys.platform.startswith('freebsd'): return 'freebsd' + # Handle for Python >= 3.13 + elif sys.platform == 'android': + return 'android' else: exit('Unrecognized sys.platform: %s' % (sys.platform)) diff --git a/panda/src/android/site.py b/panda/src/android/site.py index 7c8d22e207..ac04b2bf7c 100644 --- a/panda/src/android/site.py +++ b/panda/src/android/site.py @@ -6,6 +6,7 @@ from importlib.machinery import ModuleSpec from importlib import _bootstrap_external +# This should not be needed once we bump the minimum python version requirement to 3.13 sys.platform = "android" class AndroidExtensionFinder(MetaPathFinder): From d94b2dc5afa3cdeee95f0e60b555771df79a2067 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 15 Jan 2026 09:44:25 +0100 Subject: [PATCH 265/291] glgsg: Fix INLINE keyword on GLSamplerContext constructor --- panda/src/glstuff/glSamplerContext_src.cxx | 2 +- panda/src/glstuff/glSamplerContext_src.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/panda/src/glstuff/glSamplerContext_src.cxx b/panda/src/glstuff/glSamplerContext_src.cxx index 486ebb0cc5..5f53cf10ce 100644 --- a/panda/src/glstuff/glSamplerContext_src.cxx +++ b/panda/src/glstuff/glSamplerContext_src.cxx @@ -20,7 +20,7 @@ TypeHandle CLP(SamplerContext)::_type_handle; /** * */ -INLINE CLP(SamplerContext):: +CLP(SamplerContext):: CLP(SamplerContext)(CLP(GraphicsStateGuardian) *glgsg, const SamplerState &sampler) : SamplerContext(sampler) diff --git a/panda/src/glstuff/glSamplerContext_src.h b/panda/src/glstuff/glSamplerContext_src.h index 2a07bdd4a1..14637a4b7d 100644 --- a/panda/src/glstuff/glSamplerContext_src.h +++ b/panda/src/glstuff/glSamplerContext_src.h @@ -25,8 +25,8 @@ class CLP(GraphicsStateGuardian); */ class EXPCL_GL CLP(SamplerContext) : public SamplerContext { public: - INLINE CLP(SamplerContext)(CLP(GraphicsStateGuardian) *glgsg, - const SamplerState &sampler); + CLP(SamplerContext)(CLP(GraphicsStateGuardian) *glgsg, + const SamplerState &sampler); ALLOC_DELETED_CHAIN(CLP(SamplerContext)); virtual ~CLP(SamplerContext)(); From 0b3bdae8e895a1f33bd1a191a052612f4a6f734c Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 15 Jan 2026 09:45:46 +0100 Subject: [PATCH 266/291] glgsg: Expose native IDs for other kinds of resources than textures Fixes #1468 --- panda/src/glstuff/glBufferContext_src.cxx | 10 ++++++++++ panda/src/glstuff/glBufferContext_src.h | 2 ++ panda/src/glstuff/glIndexBufferContext_src.cxx | 10 ++++++++++ panda/src/glstuff/glIndexBufferContext_src.h | 2 ++ panda/src/glstuff/glSamplerContext_src.cxx | 10 ++++++++++ panda/src/glstuff/glSamplerContext_src.h | 2 ++ panda/src/glstuff/glVertexBufferContext_src.cxx | 10 ++++++++++ panda/src/glstuff/glVertexBufferContext_src.h | 2 ++ panda/src/gobj/savedContext.cxx | 10 ++++++++++ panda/src/gobj/savedContext.h | 4 +++- panda/src/gobj/textureContext.cxx | 10 ---------- panda/src/gobj/textureContext.h | 3 ++- 12 files changed, 63 insertions(+), 12 deletions(-) diff --git a/panda/src/glstuff/glBufferContext_src.cxx b/panda/src/glstuff/glBufferContext_src.cxx index f2e588f945..8fc4ae62a7 100644 --- a/panda/src/glstuff/glBufferContext_src.cxx +++ b/panda/src/glstuff/glBufferContext_src.cxx @@ -47,3 +47,13 @@ evict_lru() { update_data_size_bytes(0); set_resident(false); } + +/** + * Returns an implementation-defined handle or pointer that can be used + * to interface directly with the underlying API. + * Returns 0 if the underlying implementation does not support this. + */ +uint64_t CLP(BufferContext):: +get_native_id() const { + return _index; +} diff --git a/panda/src/glstuff/glBufferContext_src.h b/panda/src/glstuff/glBufferContext_src.h index dcc31c09ab..f51cd590f5 100644 --- a/panda/src/glstuff/glBufferContext_src.h +++ b/panda/src/glstuff/glBufferContext_src.h @@ -27,6 +27,8 @@ public: virtual void evict_lru(); + virtual uint64_t get_native_id() const; + CLP(GraphicsStateGuardian) *_glgsg; // This is the GL "name" of the data object. diff --git a/panda/src/glstuff/glIndexBufferContext_src.cxx b/panda/src/glstuff/glIndexBufferContext_src.cxx index 8b3e81b7ed..6971c1a4be 100644 --- a/panda/src/glstuff/glIndexBufferContext_src.cxx +++ b/panda/src/glstuff/glIndexBufferContext_src.cxx @@ -47,3 +47,13 @@ evict_lru() { update_data_size_bytes(0); mark_unloaded(); } + +/** + * Returns an implementation-defined handle or pointer that can be used + * to interface directly with the underlying API. + * Returns 0 if the underlying implementation does not support this. + */ +uint64_t CLP(IndexBufferContext):: +get_native_id() const { + return _index; +} diff --git a/panda/src/glstuff/glIndexBufferContext_src.h b/panda/src/glstuff/glIndexBufferContext_src.h index dfe011c4d0..5c1053c954 100644 --- a/panda/src/glstuff/glIndexBufferContext_src.h +++ b/panda/src/glstuff/glIndexBufferContext_src.h @@ -27,6 +27,8 @@ public: virtual void evict_lru(); + virtual uint64_t get_native_id() const; + CLP(GraphicsStateGuardian) *_glgsg; // This is the GL "name" of the data object. diff --git a/panda/src/glstuff/glSamplerContext_src.cxx b/panda/src/glstuff/glSamplerContext_src.cxx index 5f53cf10ce..84c50fb94a 100644 --- a/panda/src/glstuff/glSamplerContext_src.cxx +++ b/panda/src/glstuff/glSamplerContext_src.cxx @@ -68,4 +68,14 @@ reset_data() { // the sampler later. glGenSamplers(1, &_index); } +/** + * Returns an implementation-defined handle or pointer that can be used + * to interface directly with the underlying API. + * Returns 0 if the underlying implementation does not support this. + */ +uint64_t CLP(SamplerContext):: +get_native_id() const { + return _index; +} + #endif // OPENGLES_1 diff --git a/panda/src/glstuff/glSamplerContext_src.h b/panda/src/glstuff/glSamplerContext_src.h index 14637a4b7d..9085cc3d1e 100644 --- a/panda/src/glstuff/glSamplerContext_src.h +++ b/panda/src/glstuff/glSamplerContext_src.h @@ -33,6 +33,8 @@ public: virtual void evict_lru(); void reset_data(); + virtual uint64_t get_native_id() const; + // This is the GL "name" of the sampler object. GLuint _index; diff --git a/panda/src/glstuff/glVertexBufferContext_src.cxx b/panda/src/glstuff/glVertexBufferContext_src.cxx index 13889e111c..178d0c2b85 100644 --- a/panda/src/glstuff/glVertexBufferContext_src.cxx +++ b/panda/src/glstuff/glVertexBufferContext_src.cxx @@ -47,3 +47,13 @@ evict_lru() { update_data_size_bytes(0); mark_unloaded(); } + +/** + * Returns an implementation-defined handle or pointer that can be used + * to interface directly with the underlying API. + * Returns 0 if the underlying implementation does not support this. + */ +uint64_t CLP(VertexBufferContext):: +get_native_id() const { + return _index; +} diff --git a/panda/src/glstuff/glVertexBufferContext_src.h b/panda/src/glstuff/glVertexBufferContext_src.h index 6833ce4eec..5d5a917c48 100644 --- a/panda/src/glstuff/glVertexBufferContext_src.h +++ b/panda/src/glstuff/glVertexBufferContext_src.h @@ -29,6 +29,8 @@ public: virtual void evict_lru(); + virtual uint64_t get_native_id() const; + CLP(GraphicsStateGuardian) *_glgsg; // This is the GL "name" of the data object. diff --git a/panda/src/gobj/savedContext.cxx b/panda/src/gobj/savedContext.cxx index 92a1af46ca..41224846bb 100644 --- a/panda/src/gobj/savedContext.cxx +++ b/panda/src/gobj/savedContext.cxx @@ -31,3 +31,13 @@ void SavedContext:: write(std::ostream &out, int indent_level) const { indent(out, indent_level) << *this << "\n"; } + +/** + * Returns an implementation-defined handle or pointer that can be used + * to interface directly with the underlying API. + * Returns 0 if the underlying implementation does not support this. + */ +uint64_t SavedContext:: +get_native_id() const { + return 0; +} diff --git a/panda/src/gobj/savedContext.h b/panda/src/gobj/savedContext.h index 6c2db6aec8..ee4cc9f170 100644 --- a/panda/src/gobj/savedContext.h +++ b/panda/src/gobj/savedContext.h @@ -31,10 +31,12 @@ public: virtual void write(std::ostream &out, int indent_level) const; PUBLISHED: + virtual uint64_t get_native_id() const; + +public: static TypeHandle get_class_type() { return _type_handle; } -public: static void init_type() { TypedObject::init_type(); register_type(_type_handle, "SavedContext", diff --git a/panda/src/gobj/textureContext.cxx b/panda/src/gobj/textureContext.cxx index 7a83a17a01..767dbbe294 100644 --- a/panda/src/gobj/textureContext.cxx +++ b/panda/src/gobj/textureContext.cxx @@ -15,16 +15,6 @@ TypeHandle TextureContext::_type_handle; -/** - * Returns an implementation-defined handle or pointer that can be used - * to interface directly with the underlying API. - * Returns 0 if the underlying implementation does not support this. - */ -uint64_t TextureContext:: -get_native_id() const { - return 0; -} - /** * Similar to get_native_id, but some implementations use a separate * identifier for the buffer object associated with buffer textures. diff --git a/panda/src/gobj/textureContext.h b/panda/src/gobj/textureContext.h index 765509ec65..006f0a607c 100644 --- a/panda/src/gobj/textureContext.h +++ b/panda/src/gobj/textureContext.h @@ -37,7 +37,8 @@ public: PUBLISHED: INLINE Texture *get_texture() const; INLINE int get_view() const; - virtual uint64_t get_native_id() const; + + //virtual uint64_t get_native_id() const; virtual uint64_t get_native_buffer_id() const; INLINE bool was_modified() const; From 5c1a9574200f5c22fbce0a6a78d16d54af2c3283 Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 15 Jan 2026 10:25:38 +0100 Subject: [PATCH 267/291] gles2gsg: Add some missing OpenGL ES 3 constants --- panda/src/gles2gsg/gles2gsg.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/panda/src/gles2gsg/gles2gsg.h b/panda/src/gles2gsg/gles2gsg.h index 48d8695586..04ad8f8257 100644 --- a/panda/src/gles2gsg/gles2gsg.h +++ b/panda/src/gles2gsg/gles2gsg.h @@ -179,6 +179,7 @@ typedef char GLchar; #define GL_RG32UI 0x823C #define GL_PROGRAM_BINARY_RETRIEVABLE_HINT 0x8257 #define GL_MAX_VERTEX_ATTRIB_STRIDE 0x82E5 +#define GL_TEXTURE_COMPRESSED 0x86A1 #define GL_PROGRAM_BINARY_LENGTH 0x8741 #define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE #define GL_PROGRAM_BINARY_FORMATS 0x87FF @@ -253,6 +254,10 @@ typedef char GLchar; #define GL_UNSIGNED_INT_SAMPLER_CUBE 0x8DD4 #define GL_UNSIGNED_INT_SAMPLER_2D_ARRAY 0x8DD7 #define GL_UNSIGNED_INT_SAMPLER_BUFFER 0x8DD8 +#define GL_COPY_READ_BUFFER 0x8F36 +#define GL_COPY_READ_BUFFER_BINDING 0x8F36 +#define GL_COPY_WRITE_BUFFER 0x8F36 +#define GL_COPY_WRITE_BUFFER_BINDING 0x8F36 #define GL_MAX_IMAGE_UNITS 0x8F38 #define GL_TEXTURE_CUBE_MAP_ARRAY 0x9009 #define GL_TEXTURE_BINDING_CUBE_MAP_ARRAY 0x900A @@ -278,6 +283,10 @@ typedef char GLchar; #define GL_UNSIGNED_INT_IMAGE_BUFFER 0x9067 #define GL_UNSIGNED_INT_IMAGE_2D_ARRAY 0x9069 #define GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY 0x906A +#define GL_SHADER_STORAGE_BUFFER 0x90D2 +#define GL_SHADER_STORAGE_BUFFER_BINDING 0x90D3 +#define GL_SHADER_STORAGE_BUFFER_START 0x90D4 +#define GL_SHADER_STORAGE_BUFFER_SIZE 0x90D5 #define GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 0x90EB #define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117 #define GL_UNSIGNALED 0x9118 From 576d124b5e48e7f18a6dac5697d51c20742977fe Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 15 Jan 2026 10:13:18 +0100 Subject: [PATCH 268/291] glgsg: Support SSBO clear, assorted fixes --- panda/src/display/graphicsStateGuardian.cxx | 2 +- .../glstuff/glGraphicsStateGuardian_src.cxx | 46 +++++++++++++------ .../src/glstuff/glGraphicsStateGuardian_src.h | 5 ++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 394aa39731..409b8ce440 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -771,7 +771,7 @@ release_shader_buffers(const pvector &contexts) { * directly; call GraphicsEngine::update_shader_buffer_data() instead. * * This method will be called in the draw thread to upload data to (a part of) - * the shader buffer from the CPU. + * the shader buffer from the CPU. If data is null, clears the buffer instead. */ bool GraphicsStateGuardian:: update_shader_buffer_data(ShaderBuffer *buffer, size_t start, size_t size, diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 3c72075525..2d004a0e2d 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -1337,10 +1337,12 @@ reset() { if (is_at_least_gl_version(4, 3) || has_extension("GL_ARB_clear_buffer_object")) { _glClearBufferData = (PFNGLCLEARBUFFERDATAPROC) get_extension_func("glClearBufferData"); + _glClearBufferSubData = (PFNGLCLEARBUFFERSUBDATAPROC) + get_extension_func("glClearBufferSubData"); - if (_glClearBufferData == nullptr) { + if (_glClearBufferData == nullptr || _glClearBufferSubData == nullptr) { GLCAT.warning() - << "GL_ARB_clear_buffer_object advertised as supported by OpenGL runtime, but could not get pointers to extension function.\n"; + << "GL_ARB_clear_buffer_object advertised as supported by OpenGL runtime, but could not get pointers to extension functions.\n"; } else { _supports_clear_buffer = true; } @@ -2836,6 +2838,10 @@ reset() { _glNamedBufferSubData = (PFNGLNAMEDBUFFERSUBDATAPROC) get_extension_func("glNamedBufferSubData"); + _glClearNamedBufferSubData = (PFNGLCLEARNAMEDBUFFERSUBDATAPROC) + get_extension_func("glClearNamedBufferSubData"); + _glCopyNamedBufferSubData = (PFNGLCOPYNAMEDBUFFERSUBDATAPROC) + get_extension_func("glCopyNamedBufferSubData"); _glGetNamedBufferSubData = (PFNGLGETNAMEDBUFFERSUBDATAPROC) get_extension_func("glGetNamedBufferSubData"); @@ -7771,7 +7777,7 @@ release_shader_buffers(const pvector &contexts) { * directly; call GraphicsEngine::update_shader_buffer_data() instead. * * This method will be called in the draw thread to upload data to (a part of) - * the shader buffer from the CPU. + * the shader buffer from the CPU. If data is null, clears the buffer instead. */ bool CLP(GraphicsStateGuardian):: update_shader_buffer_data(ShaderBuffer *buffer, size_t start, size_t size, @@ -7788,13 +7794,24 @@ update_shader_buffer_data(ShaderBuffer *buffer, size_t start, size_t size, _glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); } - if (_supports_dsa) { - _glNamedBufferSubData(gbc->_index, start, size, data); + if (data != nullptr) { + if (_supports_dsa) { + _glNamedBufferSubData(gbc->_index, start, size, data); + } else { + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); + _glBufferSubData(GL_SHADER_STORAGE_BUFFER, start, size, data); + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + _current_sbuffer_index = 0; + } } else { - _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); - _glBufferSubData(GL_SHADER_STORAGE_BUFFER, start, size, data); - _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - _current_sbuffer_index = 0; + if (_supports_dsa) { + _glClearNamedBufferSubData(gbc->_index, GL_R8, start, size, GL_RED, GL_UNSIGNED_BYTE, nullptr); + } else { + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); + _glClearBufferSubData(GL_SHADER_STORAGE_BUFFER, GL_R8, start, size, GL_RED, GL_UNSIGNED_BYTE, nullptr); + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + _current_sbuffer_index = 0; + } } report_my_gl_errors(); @@ -7879,7 +7896,7 @@ async_extract_shader_buffer_data(ShaderBuffer *buffer, vector_uchar &data, _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); _glCopyBufferSubData(GL_SHADER_STORAGE_BUFFER, GL_COPY_WRITE_BUFFER, start, 0, size); _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - _glBindBuffer(GL_COPY_READ_BUFFER, 0); + _glBindBuffer(GL_COPY_WRITE_BUFFER, 0); _current_sbuffer_index = 0; if (_supports_buffer_storage && _glMemoryBarrier != nullptr) { @@ -15824,14 +15841,14 @@ do_extract_texture_data(CLP(TextureContext) *gtc, int view) { #endif } + GLint width = gtc->_width, height = gtc->_height, depth = gtc->_depth; + +#ifndef OPENGLES GLenum page_target = target; if (target == GL_TEXTURE_CUBE_MAP) { // We need a particular page to get the level parameter from. page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X; } - - GLint width = gtc->_width, height = gtc->_height, depth = gtc->_depth; -#ifndef OPENGLES glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_WIDTH, &width); if (target != GL_TEXTURE_1D) { glGetTexLevelParameteriv(page_target, 0, GL_TEXTURE_HEIGHT, &height); @@ -16819,6 +16836,9 @@ map_read_buffer(GLenum target, GLuint buffer, size_t size) { #endif _glBindBuffer(target, 0); + if (target == GL_SHADER_STORAGE_BUFFER) { + _current_sbuffer_index = 0; + } return mapped_ptr; } diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index eb5233e701..a2f4d94049 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -888,6 +888,7 @@ public: bool _supports_clear_buffer; #ifndef OPENGLES PFNGLCLEARBUFFERDATAPROC _glClearBufferData; + PFNGLCLEARBUFFERSUBDATAPROC _glClearBufferSubData; #endif PFNGLCOMPRESSEDTEXIMAGE1DPROC _glCompressedTexImage1D; @@ -1025,7 +1026,11 @@ public: PFNGLBINDTEXTUREUNITPROC _glBindTextureUnit; PFNGLMAPNAMEDBUFFERRANGEPROC _glMapNamedBufferRange; PFNGLNAMEDBUFFERSUBDATAPROC _glNamedBufferSubData; + PFNGLCLEARNAMEDBUFFERSUBDATAPROC _glClearNamedBufferSubData; + PFNGLCOPYNAMEDBUFFERSUBDATAPROC _glCopyNamedBufferSubData; PFNGLGETNAMEDBUFFERSUBDATAPROC _glGetNamedBufferSubData; +#else + static const bool _supports_dsa = false; #endif #ifndef OPENGLES_1 From f025ba5da868a0ecc700ba5cfeacc5c712d1fa3c Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 15 Jan 2026 10:49:15 +0100 Subject: [PATCH 269/291] glgsg: Fix compilation error, remove redundant parameter --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 14 ++++++-------- panda/src/glstuff/glGraphicsStateGuardian_src.h | 6 +++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 2d004a0e2d..977cbec9e0 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -16384,8 +16384,7 @@ do_extract_texture_data(CLP(TextureContext) *gtc, int view) { PTA_uchar image; size_t page_size = 0; - if (!extract_texture_image(image, page_size, tex, target, page_target, - type, compression, 0)) { + if (!extract_texture_image(image, page_size, tex, target, type, compression, 0)) { return false; } @@ -16403,8 +16402,7 @@ do_extract_texture_data(CLP(TextureContext) *gtc, int view) { // Also get the mipmap levels. for (int n = 1; n < gtc->_num_levels; ++n) { - if (!extract_texture_image(image, page_size, tex, target, page_target, - type, compression, n)) { + if (!extract_texture_image(image, page_size, tex, target, type, compression, n)) { return false; } if (num_views == 1) { @@ -16424,7 +16422,7 @@ do_extract_texture_data(CLP(TextureContext) *gtc, int view) { */ bool CLP(GraphicsStateGuardian):: extract_texture_image(PTA_uchar &image, size_t &page_size, - Texture *tex, GLenum target, GLenum page_target, + Texture *tex, GLenum target, Texture::ComponentType type, Texture::CompressionMode compression, int n) { #ifdef OPENGLES // Extracting texture data unsupported in OpenGL ES. @@ -16453,8 +16451,8 @@ extract_texture_image(PTA_uchar &image, size_t &page_size, if (compression != Texture::CM_off) { GLint image_size; - glGetTexLevelParameteriv(page_target, n, - GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &image_size); + glGetTexLevelParameteriv(GL_TEXTURE_CUBE_MAP_POSITIVE_X, n, + GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &image_size); nassertr(image_size <= (int)page_size, false); page_size = image_size; } @@ -16462,7 +16460,7 @@ extract_texture_image(PTA_uchar &image, size_t &page_size, image = PTA_uchar::empty_array(page_size * 6); for (int z = 0; z < 6; ++z) { - page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z; + GLenum page_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + z; if (compression == Texture::CM_off) { glGetTexImage(page_target, n, external_format, pixel_type, diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index a2f4d94049..af9cc85f8c 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -669,9 +669,9 @@ protected: void check_nonresident_texture(BufferContextChain &chain); bool do_extract_texture_data(CLP(TextureContext) *gtc, int view); bool extract_texture_image(PTA_uchar &image, size_t &page_size, - Texture *tex, GLenum target, GLenum page_target, - Texture::ComponentType type, - Texture::CompressionMode compression, int n); + Texture *tex, GLenum target, + Texture::ComponentType type, + Texture::CompressionMode compression, int n); #ifdef SUPPORT_FIXED_FUNCTION void do_point_size(); From 2587247868a3b9938d4d972351b249928ace7e4c Mon Sep 17 00:00:00 2001 From: "Wizzerinus | Alex K." Date: Fri, 16 Jan 2026 23:16:01 +0300 Subject: [PATCH 270/291] audio: Ability to read comments/metadata from sound files (#1656) --- panda/src/audio/audioSound.cxx | 58 +++++++++++++++++++ panda/src/audio/audioSound.h | 11 ++++ panda/src/audiotraits/openalAudioManager.cxx | 1 + panda/src/audiotraits/openalAudioManager.h | 3 + panda/src/audiotraits/openalAudioSound.cxx | 11 ++++ panda/src/audiotraits/openalAudioSound.h | 5 ++ panda/src/ffmpeg/ffmpegAudioCursor.cxx | 17 ++++++ panda/src/ffmpeg/ffmpegAudioCursor.h | 2 + panda/src/movies/flacAudioCursor.cxx | 41 ++++++++++--- panda/src/movies/flacAudioCursor.h | 8 +++ panda/src/movies/movieAudioCursor.cxx | 9 +++ panda/src/movies/movieAudioCursor.h | 2 + panda/src/movies/opusAudioCursor.cxx | 26 +++++++++ panda/src/movies/opusAudioCursor.h | 3 + panda/src/movies/vorbisAudioCursor.cxx | 25 ++++++++ panda/src/movies/vorbisAudioCursor.h | 3 + tests/audio/openclose_with_comments.flac | Bin 0 -> 453789 bytes tests/audio/openclose_with_comments.mp3 | Bin 0 -> 166596 bytes tests/audio/openclose_with_comments.ogg | Bin 0 -> 370027 bytes tests/audio/openclose_with_comments.opus | Bin 0 -> 120006 bytes tests/audio/test_loading.py | 35 ++++++++++- 21 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 tests/audio/openclose_with_comments.flac create mode 100644 tests/audio/openclose_with_comments.mp3 create mode 100644 tests/audio/openclose_with_comments.ogg create mode 100644 tests/audio/openclose_with_comments.opus diff --git a/panda/src/audio/audioSound.cxx b/panda/src/audio/audioSound.cxx index 946d1bd9f0..e3c1a11c39 100644 --- a/panda/src/audio/audioSound.cxx +++ b/panda/src/audio/audioSound.cxx @@ -13,6 +13,7 @@ */ #include "audioSound.h" +#include "vector_string.h" using std::ostream; @@ -196,3 +197,60 @@ operator << (ostream &out, AudioSound::SoundStatus status) { return out << "**invalid AudioSound::SoundStatus(" << (int)status << ")**"; } + +static const vector_string empty; + +/** + * Get the comment attached to this AudioSound as a list of strings. + */ +const vector_string& AudioSound:: +get_raw_comment() const { + return empty; +} + +/** + * Returns true if this AudioSound has a comment with the given key, + * i.e. "author". Case-sensitive. + */ +bool AudioSound:: +has_comment(const std::string &key) const { + for (const std::string &st : get_raw_comment()) { + if (st.rfind(key + "=", 0) == 0) { + return true; + } + } + return false; +} + +/** + * Returns the value for a given key in the comment. If the key is not present, + * returns an empty string. + */ +std::string AudioSound:: +get_comment(const std::string &key) const { + for (const std::string &st : get_raw_comment()) { + if (st.rfind(key + "=", 0) == 0) { + return st.substr(key.length() + 1); + } + } + return ""; +} + +/** + * Returns the number of comments this sound has. + */ +int AudioSound:: +get_num_raw_comments() const { + return get_raw_comment().size(); +} + +/** + * Returns the comment at a given index. + */ +std::string AudioSound:: +get_raw_comment(int index) const { + if (index >= get_num_raw_comments() || index < 0) { + return ""; + } + return get_raw_comment()[index]; +} diff --git a/panda/src/audio/audioSound.h b/panda/src/audio/audioSound.h index c1aa059c35..a40bbdb31d 100644 --- a/panda/src/audio/audioSound.h +++ b/panda/src/audio/audioSound.h @@ -20,6 +20,7 @@ #include "pointerTo.h" #include "filterProperties.h" #include "luse.h" +#include "vector_string.h" class AudioManager; @@ -141,6 +142,16 @@ PUBLISHED: enum SoundStatus { BAD, READY, PLAYING }; virtual SoundStatus status() const = 0; + bool has_comment(const std::string &key) const; + std::string get_comment(const std::string &key) const; + MAKE_MAP_PROPERTY(comments, has_comment, get_comment); + + virtual const vector_string& get_raw_comment() const; + + int get_num_raw_comments() const; + std::string get_raw_comment(int index) const; + MAKE_SEQ_PROPERTY(raw_comments, get_num_raw_comments, get_raw_comment); + virtual void output(std::ostream &out) const; virtual void write(std::ostream &out) const; diff --git a/panda/src/audiotraits/openalAudioManager.cxx b/panda/src/audiotraits/openalAudioManager.cxx index d45060c33c..451cd172f4 100644 --- a/panda/src/audiotraits/openalAudioManager.cxx +++ b/panda/src/audiotraits/openalAudioManager.cxx @@ -440,6 +440,7 @@ get_sound_data(MovieAudio *movie, int mode) { sd->_rate = stream->audio_rate(); sd->_channels = stream->audio_channels(); sd->_length = stream->length(); + sd->_comment = stream->get_raw_comment(); audio_debug("Creating: " << sd->_movie->get_filename().get_basename()); audio_debug(" - Rate: " << sd->_rate); audio_debug(" - Channels: " << sd->_channels); diff --git a/panda/src/audiotraits/openalAudioManager.h b/panda/src/audiotraits/openalAudioManager.h index 56b01afc6c..236dc92f23 100644 --- a/panda/src/audiotraits/openalAudioManager.h +++ b/panda/src/audiotraits/openalAudioManager.h @@ -21,6 +21,7 @@ #include "pset.h" #include "movieAudioCursor.h" #include "reMutex.h" +#include "vector_string.h" // OSX uses the OpenAL framework #ifdef HAVE_OPENAL_FRAMEWORK @@ -173,6 +174,8 @@ private: int _channels; int _client_count; ExpirationQueue::iterator _expire; + + vector_string _comment; }; diff --git a/panda/src/audiotraits/openalAudioSound.cxx b/panda/src/audiotraits/openalAudioSound.cxx index d334a4cd3d..8b404f1cba 100644 --- a/panda/src/audiotraits/openalAudioSound.cxx +++ b/panda/src/audiotraits/openalAudioSound.cxx @@ -17,6 +17,7 @@ #include "coordinateSystem.h" #include "openalAudioSound.h" #include "openalAudioManager.h" +#include "vector_string.h" TypeHandle OpenALAudioSound::_type_handle; @@ -88,6 +89,8 @@ OpenALAudioSound(OpenALAudioManager *manager, audio_warning("stereo sound " << movie->get_filename() << " will not be spatialized"); } } + + _comment = std::move(_sd->_comment); release_sound_data(false); } @@ -1092,3 +1095,11 @@ status() const { return AudioSound::PLAYING; } } + +/** + * Returns the comments attached to this audio file. + */ +const vector_string& OpenALAudioSound:: +get_raw_comment() const { + return _comment; +} diff --git a/panda/src/audiotraits/openalAudioSound.h b/panda/src/audiotraits/openalAudioSound.h index 6dc30a936c..48394bd175 100644 --- a/panda/src/audiotraits/openalAudioSound.h +++ b/panda/src/audiotraits/openalAudioSound.h @@ -19,6 +19,7 @@ #include "movieAudioCursor.h" #include "trueClock.h" #include "openalAudioManager.h" +#include "vector_string.h" // OSX uses the OpenAL framework #ifdef HAVE_OPENAL_FRAMEWORK @@ -117,6 +118,8 @@ public: void finished(); + const vector_string& get_raw_comment() const; + private: OpenALAudioSound(OpenALAudioManager* manager, MovieAudio *movie, @@ -214,6 +217,8 @@ private: PN_stdfloat _cone_outer_angle; PN_stdfloat _cone_outer_gain; + vector_string _comment; + public: static TypeHandle get_class_type() { return _type_handle; diff --git a/panda/src/ffmpeg/ffmpegAudioCursor.cxx b/panda/src/ffmpeg/ffmpegAudioCursor.cxx index 69635549a6..89301ffa17 100644 --- a/panda/src/ffmpeg/ffmpegAudioCursor.cxx +++ b/panda/src/ffmpeg/ffmpegAudioCursor.cxx @@ -15,6 +15,7 @@ #include "ffmpegAudioCursor.h" #include "ffmpegAudio.h" +#include "vector_string.h" extern "C" { #include #include @@ -508,3 +509,19 @@ read_samples(int n, int16_t *data) { _samples_read += n; return n; } + +/** + * + */ +vector_string FfmpegAudioCursor:: +get_raw_comment() const { + AVDictionaryEntry *tag = nullptr; + vector_string comment_strings; + while ((tag = av_dict_get(_format_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { + std::string comment(tag->key); + comment.append("="); + comment.append(tag->value); + comment_strings.push_back(std::move(comment)); + } + return comment_strings; +} diff --git a/panda/src/ffmpeg/ffmpegAudioCursor.h b/panda/src/ffmpeg/ffmpegAudioCursor.h index 77e8890d6c..02c53df700 100644 --- a/panda/src/ffmpeg/ffmpegAudioCursor.h +++ b/panda/src/ffmpeg/ffmpegAudioCursor.h @@ -21,6 +21,7 @@ #include "texture.h" #include "pointerTo.h" #include "ffmpegVirtualFile.h" +#include "vector_string.h" extern "C" { #include @@ -43,6 +44,7 @@ PUBLISHED: FfmpegAudioCursor(FfmpegAudio *src); virtual ~FfmpegAudioCursor(); virtual void seek(double offset); + vector_string get_raw_comment() const; public: virtual int read_samples(int n, int16_t *data); diff --git a/panda/src/movies/flacAudioCursor.cxx b/panda/src/movies/flacAudioCursor.cxx index 0927f54b5d..1af4121146 100644 --- a/panda/src/movies/flacAudioCursor.cxx +++ b/panda/src/movies/flacAudioCursor.cxx @@ -24,8 +24,8 @@ extern "C" { /** * Callback passed to dr_flac to implement file I/O via the VirtualFileSystem. */ -static size_t cb_read_proc(void *user, void *buffer, size_t size) { - std::istream *stream = (std::istream *)user; +size_t cb_read_proc(void *user, void *buffer, size_t size) { + std::istream *stream = static_cast(user)->_stream; nassertr(stream != nullptr, 0); stream->read((char *)buffer, size); @@ -41,8 +41,8 @@ static size_t cb_read_proc(void *user, void *buffer, size_t size) { /** * Callback passed to dr_flac to implement file seeking via the VirtualFileSystem. */ -static drflac_bool32 cb_seek_proc(void* user, int offset, drflac_seek_origin origin) { - std::istream* stream = static_cast(user); +drflac_bool32 cb_seek_proc(void* user, int offset, drflac_seek_origin origin) { + std::istream* stream = static_cast(user)->_stream; nassertr(stream != nullptr, DRFLAC_FALSE); std::ios_base::seekdir dir; @@ -64,14 +64,30 @@ static drflac_bool32 cb_seek_proc(void* user, int offset, drflac_seek_origin ori /** * Callback passed to dr_flac to report the current stream position. */ -static drflac_bool32 cb_tell_proc(void *user, drflac_int64 *pCursor) { - std::istream *stream = (std::istream *)user; +drflac_bool32 cb_tell_proc(void *user, drflac_int64 *pCursor) { + std::istream *stream = static_cast(user)->_stream; nassertr(stream != nullptr, DRFLAC_FALSE); *pCursor = (drflac_int64)stream->tellg(); return !stream->fail() ? DRFLAC_TRUE : DRFLAC_FALSE; } +/** + * Callback passed to dr_flac to process metadata found inside flac files. + */ +void cb_meta_proc(void* pUserData, drflac_metadata* pMetadata) { + FlacAudioCursor* cursor = static_cast(pUserData); + if (pMetadata->type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) { + drflac_vorbis_comment_iterator iter; + drflac_init_vorbis_comment_iterator(&iter, pMetadata->data.vorbis_comment.commentCount, pMetadata->data.vorbis_comment.pComments); + const char* next_comment; + drflac_uint32 comment_length; + while ((next_comment = drflac_next_vorbis_comment(&iter, &comment_length)) != nullptr) { + cursor->raw_comment.push_back({ next_comment, comment_length }); + } + } +} + TypeHandle FlacAudioCursor::_type_handle; /** @@ -83,12 +99,13 @@ FlacAudioCursor(FlacAudio *src, std::istream *stream) : MovieAudioCursor(src), _is_valid(false), _drflac(nullptr), - _stream(stream) + _stream(stream), + raw_comment() { nassertv(stream != nullptr); nassertv(stream->good()); - _drflac = drflac_open(&cb_read_proc, &cb_seek_proc, &cb_tell_proc, (void *)stream, nullptr); + _drflac = drflac_open_with_metadata(&cb_read_proc, &cb_seek_proc, &cb_tell_proc, &cb_meta_proc, (void *)this, nullptr); if (_drflac == nullptr) { movies_cat.error() @@ -147,3 +164,11 @@ read_samples(int n, int16_t *data) { _samples_read += n; return n; } + +/** + * + */ +vector_string FlacAudioCursor:: +get_raw_comment() const { + return raw_comment; +} diff --git a/panda/src/movies/flacAudioCursor.h b/panda/src/movies/flacAudioCursor.h index 93a48e5f32..349b004724 100644 --- a/panda/src/movies/flacAudioCursor.h +++ b/panda/src/movies/flacAudioCursor.h @@ -16,6 +16,7 @@ #include "pandabase.h" #include "movieAudioCursor.h" +#include "vector_string.h" #define DR_FLAC_NO_STDIO extern "C" { @@ -31,6 +32,11 @@ class FlacAudio; * @since 1.10.0 */ class EXPCL_PANDA_MOVIES FlacAudioCursor : public MovieAudioCursor { + friend drflac_bool32 cb_tell_proc(void* pUserData, drflac_int64 *pCursor); + friend drflac_bool32 cb_seek_proc(void* pUserData, int offset, drflac_seek_origin origin); + friend size_t cb_read_proc(void* pUserData, void *buffer, size_t size); + friend void cb_meta_proc(void* pUserData, drflac_metadata* pMetadata); + PUBLISHED: explicit FlacAudioCursor(FlacAudio *src, std::istream *stream); virtual ~FlacAudioCursor(); @@ -58,9 +64,11 @@ public: return get_class_type(); } virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + vector_string get_raw_comment() const; private: static TypeHandle _type_handle; + vector_string raw_comment; }; #include "flacAudioCursor.I" diff --git a/panda/src/movies/movieAudioCursor.cxx b/panda/src/movies/movieAudioCursor.cxx index a836937455..374b7a9f93 100644 --- a/panda/src/movies/movieAudioCursor.cxx +++ b/panda/src/movies/movieAudioCursor.cxx @@ -12,6 +12,7 @@ */ #include "movieAudioCursor.h" +#include "vector_string.h" TypeHandle MovieAudioCursor::_type_handle; @@ -167,3 +168,11 @@ int MovieAudioCursor:: ready() const { return 0x40000000; } + +/** + * + */ +vector_string MovieAudioCursor:: +get_raw_comment() const { + return vector_string(); +} diff --git a/panda/src/movies/movieAudioCursor.h b/panda/src/movies/movieAudioCursor.h index e7c9668713..b810b04a59 100644 --- a/panda/src/movies/movieAudioCursor.h +++ b/panda/src/movies/movieAudioCursor.h @@ -18,6 +18,7 @@ #include "namable.h" #include "texture.h" #include "pointerTo.h" +#include "vector_string.h" class MovieAudio; /** @@ -49,6 +50,7 @@ PUBLISHED: virtual void seek(double offset); void read_samples(int n, Datagram *dg); vector_uchar read_samples(int n); + virtual vector_string get_raw_comment() const; public: virtual int read_samples(int n, int16_t *data); diff --git a/panda/src/movies/opusAudioCursor.cxx b/panda/src/movies/opusAudioCursor.cxx index ab81068742..afdf738f26 100644 --- a/panda/src/movies/opusAudioCursor.cxx +++ b/panda/src/movies/opusAudioCursor.cxx @@ -17,6 +17,7 @@ #include "opusAudio.h" #include "virtualFileSystem.h" +#include "vector_string.h" #ifdef HAVE_OPUS @@ -121,6 +122,20 @@ static const OpusFileCallbacks callbacks = {cb_read, cb_seek, cb_tell, nullptr}; TypeHandle OpusAudioCursor::_type_handle; +/** + * + */ +vector_string +parse_opus_comments(const OpusTags *com) { + // Blatant copy from vorbis code because they're the same struct + vector_string comments; + for (int cnum = 0; cnum < com->comments; ++cnum) { + std::string tag(com->user_comments[cnum], com->comment_lengths[cnum]); + comments.push_back(std::move(tag)); + } + return comments; +} + /** * Reads the .wav header from the indicated stream. This leaves the read * pointer positioned at the start of the data. @@ -156,6 +171,9 @@ OpusAudioCursor(OpusAudio *src, istream *stream) : _can_seek_fast = _can_seek; _is_valid = true; + + const OpusTags *tag = op_tags(_op, -1); + _comment = parse_opus_comments(tag); } /** @@ -270,4 +288,12 @@ read_samples(int n, int16_t *data) { return n; } +/** + * + */ +vector_string OpusAudioCursor:: +get_raw_comment() const { + return _comment; +} + #endif // HAVE_OPUS diff --git a/panda/src/movies/opusAudioCursor.h b/panda/src/movies/opusAudioCursor.h index 3e2137de5e..26dea22a63 100644 --- a/panda/src/movies/opusAudioCursor.h +++ b/panda/src/movies/opusAudioCursor.h @@ -16,6 +16,7 @@ #include "pandabase.h" #include "movieAudioCursor.h" +#include "vector_string.h" #ifdef HAVE_OPUS @@ -37,6 +38,7 @@ PUBLISHED: explicit OpusAudioCursor(OpusAudio *src, std::istream *stream); virtual ~OpusAudioCursor(); virtual void seek(double offset); + vector_string get_raw_comment() const; public: virtual int read_samples(int n, int16_t *data); @@ -47,6 +49,7 @@ protected: OggOpusFile *_op; std::istream *_stream; int _link; + vector_string _comment; public: static TypeHandle get_class_type() { diff --git a/panda/src/movies/vorbisAudioCursor.cxx b/panda/src/movies/vorbisAudioCursor.cxx index 181b9454c4..e7954d2bc5 100644 --- a/panda/src/movies/vorbisAudioCursor.cxx +++ b/panda/src/movies/vorbisAudioCursor.cxx @@ -17,6 +17,7 @@ #include "vorbisAudio.h" #include "virtualFileSystem.h" +#include "vector_string.h" #ifdef HAVE_VORBIS @@ -24,6 +25,19 @@ using std::istream; TypeHandle VorbisAudioCursor::_type_handle; +/** + * + */ +vector_string +parse_vorbis_comments(vorbis_comment *com) { + vector_string comments; + for (int cnum = 0; cnum < com->comments; ++cnum) { + std::string tag(com->user_comments[cnum], com->comment_lengths[cnum]); + comments.push_back(std::move(tag)); + } + return comments; +} + /** * Reads the .wav header from the indicated stream. This leaves the read * pointer positioned at the start of the data. @@ -67,6 +81,9 @@ VorbisAudioCursor(VorbisAudio *src, istream *stream) : _can_seek = vorbis_enable_seek && (ov_seekable(&_ov) != 0); _can_seek_fast = _can_seek; + vorbis_comment *com = ov_comment(&_ov, -1); + _comment = parse_vorbis_comments(com); + _is_valid = true; } @@ -313,4 +330,12 @@ cb_tell_func(void *datasource) { return stream->tellg(); } +/** + * + */ +vector_string VorbisAudioCursor:: +get_raw_comment() const { + return _comment; +} + #endif // HAVE_VORBIS diff --git a/panda/src/movies/vorbisAudioCursor.h b/panda/src/movies/vorbisAudioCursor.h index cd74acc750..2bb48dfa3c 100644 --- a/panda/src/movies/vorbisAudioCursor.h +++ b/panda/src/movies/vorbisAudioCursor.h @@ -16,6 +16,7 @@ #include "pandabase.h" #include "movieAudioCursor.h" +#include "vector_string.h" #ifdef HAVE_VORBIS @@ -33,6 +34,7 @@ PUBLISHED: explicit VorbisAudioCursor(VorbisAudio *src, std::istream *stream); virtual ~VorbisAudioCursor(); virtual void seek(double offset); + vector_string get_raw_comment() const; public: virtual int read_samples(int n, int16_t *data); @@ -51,6 +53,7 @@ protected: OggVorbis_File _ov; #endif int _bitstream; + vector_string _comment; public: static TypeHandle get_class_type() { diff --git a/tests/audio/openclose_with_comments.flac b/tests/audio/openclose_with_comments.flac new file mode 100644 index 0000000000000000000000000000000000000000..a32b3836e8eec20434a1d25fd0497d134bdababe GIT binary patch literal 453789 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�g zM0Kv^(uyuK$#zl*?zBXf4z`{2JR+&y<}3`<=F=s4+iwt)S0(s{F@v(kA#o;=F?MsG zJO|@V5}Hxj#zEuU)i?GHhV3$u_e)o&PjuKN>IHzGr8S$e5XjjtwN;E*;9j9iA~qF0 zx0wf6!@(DcK2t!cZO?zwTW2oRrIJMwW-r;Wsl3=U12tZHJ6`3nGTF#S&HB+VOLOAA z=1dhP914!QS~;?4n3=6nF#SGs8Za9aN>dz|t9qTy)0eS(JCQ84GEOTa1V=N*dP8O} zA~|j=OJ$*YBN$`<(GP66!%B)b6S%Jtmm$KCy~}Y}rBBuYNMyb*!)=8Z>e@n)rMqzA zcD5$L8lXmGk<=uKn!Xjx6~?e5@f%s~z+jU9_{o?KUQPfUfC0b$ANtSn*Yp$pCIHT~ zpp6SPw4bXUB96D7hR;kyWy+6AMCpJetAUAn6@Z(7=X@R5%86NE;8GRjLceX7vFOgk=q3P;fA=60{}~O(e(QvVSm6Jn=#> zSW`h;{9_K`5wK&Yx}}JH6WbcVK`;oz@1*OSi#IW9=pJT`!3PPNuM1Bp>XApgZG%dS zSM4+_viD6LZ6^r+(6dT_bjz@y8r8?*T1)dcW_=klmI`FCV>=767`VWJW>COe3ukPH z`@1h-y6Ys!wl1Q0BD<^;Iw^PR>_s=eT7z~{6;;NDvlDRY|1zS^Q4?}Y6of^>U4Pwa zur@6;dP(HqhI_c^uhg;oRt9MHC8XI3!nJ6f3z6}BXFSrDa95waw=z#DIbQQL^v(o2u_lT zNC^PEI1&f1hdg5-jbr_JXCNa7ff4%`to34YsSSRY`g0_an8*@FxxsI*k!Kh{J(x66fo$N}s8V|>xxqk?U+1RHcCUJ!BCk^37E6phJa#(1M@Et_&2cyNA*^8r@T*J5Flx2lt3iIUwcgBGm2~WXCGTZm}eezV4uLyLoaflf3yK$}Ot;4O2Ij zal!NFK$2p6NNiu7XtmRVqTYdwph#O)a3;!r#%B)Q#^C_ysF}9mT-TDwFL}Hy=O^@a z6I8E~CA>9kmP`UMO>M1YkA*gjB%azy%xW#7>?~S+dCKZi4$ljF@>h2hvoz*kMcsvl8P9ju#`$}~Y|YjT7!kOWixL#_k4XaHR&h2924kyGq9c|1#S{rR ztTxofbH}|z>yJ*GhqPe;q~X=;4LSpQpgH5kM2gh9VEWIyjSP9vfr^2u(#4E#AW8E zZ7h-IPS6tNjm4KUV(y_BkL{X=>Y&6J56e%h)FY6gWwEYz(B1^0`N{C!p0mN?d_qtJ zz?ySApx%eGN8J8vXJ#=1P(f5=GK{Rl2LMM-q6SekD2~e2A$bh}eENApnS+UJMJS+! zM&ZK7HLS6dhQW<#r73ofCC2Nc*rix4Itd~T_D)}usJr#FJ0iZk7NR{@2x!=*@| z+~$fw93uSulyKE0Zr>Re%j=eu!$wM>RT?G^1}PuOG_}K|jLFxqSFYsOJZ;K6mUg8x z6WaEs+6v`h+=X5f(m#fk*I`9mhCCA-1`voPKfbXf&|1^&;=<}NY(g^_9vOHE=^RCM zZr)`29v?iL^UUk-i?c`Aqc$@1+v~e8bsh55sYvD1NRw{#&5TmlUvbt$??vw0mhJng zbUJ;K%}dDbAl2F>ulQYQU_BaCu$c?K}0}L+ODQBip3XF zI*(ZHeMZz*Z0r-sMDh}o=%zXrlAPI5P9%k+dlS`^B)zO?CMcE7 zSrPM!&risvjxQCtX4yd{Bm6lmI6PA;4255A(*qF|jfTwrAWf993??Ng^f+-6Gtq?l?nj~&I>KjEl z3D*Eqs7dR^zJmz`8OX$%nMu4>Pv+b>gj-T8m&gmL|CI6hHEKx{8cj#{k_#adHY69` zFX18hvUYH5XL{|o3&;vW`E4{xETxpt$aVoOD_m7Rud#^5rX+%8G%+WrO#%Z8%R^%V zRqCTK5ruED1#e~H7g->uRjQs%9&%zEKNAL@8BV`n5F2Us!y?XM2`O1Jy}Umo7Nr5f z6O7_cC_O1|jOG_Fv3)Tfgox2cUL#Y40v0TW%B8BluA!&Zgeo-1@+nk>Stlo!+EVEv z;h2@=5L4HxiLRxlED0#uudFf?)xL;pMa26rad^jM`CKXfXX){_k^8Mr(#0{{x4*wZ z(p9|HJr%{>Nrk$j{G`BBWB#d<3K{ZxQOg@>^xsv=+Z6{JwKRfwqvUN;5Rac9Yj?aF zp$5gE!R@eSoAMB5nl?$oO6}|D;#G)Ls*!d}|6-7Fc71(T4OD3Xyi?ygx@8tsjs{21 zsR*`b(~?s5Y>sb&s<9%jeQCv=P0A&Lxkz9w60*l-Lq(kC-^N~g5zB6S6T$S6x;DA9 z6kkl{vQ&lkV-Rc?1P{$*;#s8>r=e+6k*VZ@zFHp*S*CNkaiYy#_Cm!!sY{`7Q@Hl1 z2)2)QDHES(9nhCDesUM3DjSi*Ya@)d|0r_pEy??*Qs-B~1%>>x#YQB(_(_YZRaIh$ zsFSJ6Rd`gnnwajz;viu)xonn;jJ`n;8&F4H9tR}sJ{Qz4SwM)_bt@dk_BwB2QIvrt zillB6`Ji}3CS#BZ#cW`f!;>gBjYabgx^kL*R$X#6UfJJ=(&ej>l`P%vVi~lAl+ytU zRr96cn02I+-|U{-L`xgDzWB1_OG5~sVnR zkv!iCMuKQ0I@t?P5UQ_;3CD|w*h`KqWr|S4D3!v5WJotf(+_B{Y2`R^f>^P13BnxC z?#dojeHK}v@e}YJ&KGfrg9>hol37mSK;$h!fjr#{PEwHcA>Q9Tfg~cxLkMGxY~=Vu zDD7pG;Vh9rg%O8FNIXPzt34>qEhI&#_W5X?M><%9D^<@Pp)?ZYaUd{7a#d0hf*8DEL;|VlQ$USt@tkKP=oF%+uEiD3>MI3rI~H=nAu6STO#mg zkYd;)=JAd({mSD*A0iq^A;SUcT#ti=@Gp&0JI~-ThY6q~0yJNLR)R@ubGfDvgiA~@ z1Z?RMz84w~)dqN;oD<<3i6Bp8xs3KE+zfVM^c*CUuwRKx`9PjG?y#953@=vl<8wUsD}@+g{m@3@d3#TcD2_Nb=O?AHefK(w@7!|6- z%TQlWDm_Tj5{}rW$Ij}C8x3wGb55|Pkl60Drmidv#nqywC5g2W9;%#6z8}@z%@<4- zZMR?V;9Yt$k!!wq$z2lp{;J02&ygj3jRVFCy@8cyT$hd zdQHpg0*H&1Tz>YxIvfcHj@~S~qGQ=Ki%1iQW<(;^W;#RBHBF-+qGv2GtJElc%ns{r z`%RC+g9B&OrLm^N9ZD-ivrDQ_ zzA6#SWjidAEjF*4P3YF4WSDzNv{H}9602~w)|wDbrt{E~(JmSctp*cQ4Io8iHZMrN z1&4L!ca;tkdpr8Mv-^bTM@O`@GkcK;PsJ+sB_aJzqEISr5Sz$#R@z1#h@!T$G)Y!N;&Upfg-oI@v8RgBS4QCWgN|d7vv?TY3(?YQ@Hj1K z>P!`#v=&tH$|m<;%!yo7TBaW(>0y2&$e)7YSwW}!XJy6onq7*%b1~(0p07{Q_H=nx zAEG4`S|*{DX?)nk(sNl#F4ZK{+W8vCP1N#RcWVidhuAc}5@cUptj@Bj2(bxLP4r%E zztjHmuo7-9!R9()3ooauw~sa^JIRpO9<;*$_-iMVqC6h^dV?do9F zPC^7CNLG{D?MGV&J9ErqVi)5l3{WVTbuke#m&Nm=9G`xn4QohRNvtczF-dT#G4spD zl^m2LZO8J-r3PP`y-$wod74VPSCV7Pgz>?4Q+#Mm!`k=|n$eDvhY5LQr4Jo30<3x< zB?$KF;I2?*M%sK}Fb(bnt7eR*EUP=PB@!LVgL<61R?r*7hvTZab#E$tyP<~I?p`mM4$VdlBy~#eAn<_ZYOPF?N z)fs|Ztpu_{_7!<;zvTMHL!3RQu{Dc(z1<%M#8OO>CSH)9j#~a^SIArh=sL;>tzVc;jXau`UDW!ZaxL=@NG{hNxP=G25aHcXL+*9K1QhD#L` zfVclTQlfdae?xN_uj;7K<5`!t10#tSf2()@Ywe_uDc9v;mB-&rg0(=?WlO3^_yXgx zQ}b(*fk5pbI!JU{$kv`w#7k-O$1V_vG@S8N{Eo|}g`uDPD6TqZP{rX1QxV-qi)h|= zQ(MzFjckOS_m(W`P>e`ir+?=FYyb9u4*@=bm;l)TpxO%7+m~)|RWlREJKe)_7s*%5 zEDPP9Dx)zvgkv16DXV~*ZKu%E$0To)n4j97k_gMTOEX}a;#xa9R8Ss46p7DIMjMkC zPDf3)WVpufZt_AD7?_FzzA}ScU4OS{6s32TE2x*@z(<~wWPKOA6N+Wfb=swzW@hon z-I9_ct8*SZd>Q3L^QSdtOgS&y#(E8q@jVhkS7}c;Br9wd?Q|kFl&&M=UZ|tNo`_DK z?gad!h(_?EWW;tpN)*dURp~vARLUNtS42ReL@(tWS*+P~+0>PYD4o&=%NH@=LiOVH zifpZV(?=7UJ@l`C>gk=0E?SpD@%y(7R@P@#Ku zXrb_~)cI;wbHlcw8qa7!RbeV|?(~anDM~%mb=O5gR#uJy{k;SC=e-|&NY#tCf+sL6 zq1iZEm|h8ZNzHbW!%CwzdYa=QD+gtsjPAO!R>a>&NO9%IWB6}ksX7oG{)n`#=qPKvvmexpi@uPbJkV!dVfnWI@ZH7sN$>EviSChp_qN#r8FX6Jtb zKT#mH&-!2|CbsbD>TD7X5fzmTCmEaTJ&5m{=@p7E_nHsfP3$CI^5~p`sX#GUBFdb=c1b6?A#xHRRHhN^3eNmj)!i`BAzxQXd(Dx(_9qIkX-AZ6 zWg&A-x6P4?OH*}!C~FK!u5uWWsNf<}KGbYsgYBF^*h}b%GOApk8eP-RQkncQc+yE& zyq~cJAn@vnNK&B&3fJ<#CPeK_vI0ex(uV$ROQ?c!zV=2!R6R26(2JRpj@l2!XkRsX zMw4V>K&Dp6+#^@~yq2s?u1anovkKB4ITU;(Dz_7)d#&kldXnsFJ9;WVOwwp7Qu*C? z#Y(ymx~0}i&0*bN8%uv?g)+*HF3#l_6M1dj)^Z5)Tic_Enjn_l)79fv74y0&5j=?S zX=`3IHX>_Pr^z&fN2&aawKw;$hl)6!wid#AP|6p03}IjBDUf&q=p(s>5e$|Rn6fXEvR0Z32<0C*FCfFe{fbdE$boA$7s z9)vc5d%##RNiI1f6gL-rU7p8#`RWLkZ9g@z3g7*3D)DKeClbe-M0E8dn2P3U zrJ{g@idP4L5hw_qsRSzn@ERH)gyo==jgTor_Kj2^0#Zs4jKn@49*B-Q$eTw*fPeXT z5dL5Yzd=;K3mdjP5XIpzq7w$9?CL7O$vfGb;V|iVPPo!i@F53bZL}p5P{@WCv-orQ z_}kQ~+c5ImO!7UBBEM`Sik>|1h(4|b_=yZ*aws8$s5?vmQVuit+{k!>Cx_?f0n2`Unv20d$F8;F1$xZKbO8z}HTgvXGziH!L8caJ7 zCq5vwyJ5DBVNY~Cp@~ndFb9PdNYl5?63Kf=fAUrSrvcJbBOAyJ>b|N*dnRKQwOLEa z`p3a1sWj7svi^w)6pIyl+D#M`m}~Y9P@i!rY(;&;yxM~zCkC>*)iO?Ui;AiJJyID3 zW-atxY-QYgSAR2iFviixn>#v*H_t7JPHyj&x$?(f{r88ClpA$5eL(tWR7qbHl^oJ^ zt#Vsc90p4CsK8sE62Qj2A+cQFIQC)7u1;rw!AAWk3`ZQ zy;PG?ev;Yg!_7ELb4u6uAcp%b{s|KrtKO2;ay?G#sdH12iR}C$CHBHemoXJN+MVwSoQ zBzEwT6FW`vkW%sgbG3%YuJK;PmVrwT7Jj{>OW9TYl7tZm%D#JPqHeZX&Mo;13sUZ> ztA0#6B(&3I@c=JKpWf+6w6*hJn{7a>X7kU-=3?bWJF7Q=5Jt}jAxPM8WRY*mP+A|X zd4j}9B{qHYhxyQGqpkj7fdb}I-8T7Pl-TpW<6^an3~A_h#6of!Tyha2KPiZJunIvk zRQ89%Q{u&pIglIzvvwN{OoXIPF_gUM2nR|V*oNe)#Yob4WfqNCX+iu<&!WPe?l^;1 zBo&Syk*9WtCJccvv1T%&cMr{xx&DgKEGSjPz@&wG#|UdCV33t2f|7+-1}0e;G@!nF=;&P7OMK(xD$~lCS}>%z*r<@N+J{Xo=HuA8_cO< zO8r#$1o39?l=V>_eo;0`1gfXssf?;U?!YIiz93C;T`ONJQw(y6^dN#cvqjC|E8-v~ zpD!uBJ-`IyWhNxkp{sF%EEeR`AR%DQ^FZL~==oD?=OHLs_aKE(jKM&po9xi!ZWEH; z1Z_hhN&zd3kkt52c5y5esUH-!;-=(3CMROH*q}xyETz`+)Og30O`Xd2z6TThN~OY3 zf{Rf!*qyeqY@%%RV9;&pDj?-$?_q}!@!`UOZSj#h4W2@QbGcBy>JY9v3WJUHMX@sBw{hickzt}ZX~3aGPTFhKca9aRYLI^-F}cD!34vN#4er3aJCKZv16a7cC_M`ISKotZp>jw}gC#ve0#5I8HYd zf;~c}6Sg8dQElXAM#Ru3+%9J>PBz$MiaJoCM9N%PUw%;}?kuVcNITj{;{>w%0jx#f zrjRcZ*Lp&F*<^@CD~gE3p@^T}AsQO0T`e$t@ z&q9%zk5L0r8H<<5n4gRSguef%BPm0rV6zyAkeuRO`3aSxVQ(3)8Tx{rbBv3e+9tj6 zT}l(Ct-7w&+d847&TCsLDLa!*yx{rO9CJ9d!#7O|Wfw9`SPCd197|Mo-2`=Qs)~f1 z>P;@JxD=%br&OM>J!dD3Cm+g?(?V(P%4~l}5q0H%QN!4l+EbS8mX|T-ilw207*D4+ zYvgAZQ_EU&B@>v+=96C!lPLC0`?+ysBal{8eq?E9Ex5ZIJ5?51(kHkQi+@wp$D_hr z^Sdi|ga~I>GF(x?Fv4#b@YukB;v$h;Sb`Um=z>S6$rmHoU0?V@6BxqkI-NlxN^cS8 zs8lsBL$+~!cPb&;Wp)ssTzESvKZvNh8H?DNU*Gct;?qkBbzH!} zfQT=bNZAL&@!XR~3$+myafsEbfnmj#0VGg}!WhDAqjl zw%-x-DN?4b@f{SsH}-UK0LCUnPZaExm;IKPdnFmz6DiD3+AnWPe)62h`GJ# zm!|3WqjN^;k<;%dVwed85=4)Ws9h?f^3^I8V!16W%|*!Q5*64m%R6`Skr^gV!FOj6 z(@D5woI#L=kdbTYHm;u3pL!BJgB$9$YppzUe=Lw$G709<@UoSF#BdbO&aMdgpn})`>1QpkC^A+xdJTn@Dt#+lj{HGK ztb1!w4+x)@3vCVcQ_Ph<^KtDQiBd({p+iw))fFaeZYx{xlUl9Q<;3^TA)ZdDqZV9~lYa?Ff{@Y4G>U5}nljF1<%pQZnI}UkHjffd9V#_{pdbpiTlm z0{H?(0IC18|IPph|Le4+oVhoAd@%*tke8hX!8#gfR12L74IWsIl5)Exb*eH7RHags zr7eY{W1M&5BSnhcZL3XHY>-865I!(`K~?C~x73u%TNIlRSSyk1Nh0W=K{4X(UW8)# zuAP@Ya!v1UR20fkg}p2uSMEwX*%vh{zeqOGM88G@3E!}Ys-7uR4Kp4P`08uEH9ZsG zg(K;7N-`?9)1i$iL!r|EqX_NuUG=7_#3CU`aN0u%f)d|GZj6tQ#sebJ7I18UQB04l zp`#GYLR}=g3W&Z+Q=mIWVhbGvJmWV3?h!$*TeODZaV$sVHgy^AClc$iTTCPpO}8zS z)qJU>*{hio*uv{b$~x;`KmDUN*1u?Nzjv|ZQGG8KA*vAz+{Tj7_-(p)L?#%BLxD@x zz2oqNb6N~ZO-JLfMlC@5*nC23IT;ZKqB5{37mR<;2St4&g4Y3AP?#RIi2j8n#XSKT zDR5+95(Y!rMAS@0O*0x1ehbP)kHdblg*E()k@}RGqmz)s+?C?$OGO3v2QMAh2w^Vt zqwN(e>#0Vn^V@_GWr_+L)W;k{BNF)-un1z9g{`fNIvF$ao+3dc2=-4$H8WEBsG+mj z^C0SZoer-FEb&X!{;fhAOmVAXtfT4cly(P&P`}OUC0JHkWFowQb#Mn_K=x)z6rI7j&ViUZ?TyMxoOW0^l5lDLp(Z3{J zd|XXG6bfoOWt)*GB3R)z>5@?1mu3MvR@@dlQ`AD@QH778sUk!=ZB!_vZ&}L9L=+VJ z;-9+4mFcCnc><#Vmt?hFB^wkjt!iz;M0S0WFy@P2vRIn3VsI*w;$Q3CUvyxaTS`vm zPgT|IhjRzruWzY_?j-at+UY-h^@%2+z*@fv*^>98BZW@T=>&rFs$qi55s3+Ty6m=N zX3Z;|q?Hz~tTZa1s}-R%(2pVNkG068yS^F_%ep*u-7@>O@vadYtN%SwDNvVN!+Gss z@Z#RFLm5h%36rq1jJSx0bwk##9Q+)T#k^0~7;K_sy+i7u2+#Gx2QF7sl`=0z@J8yY z#IdS}#OvBVad=F9zWJ*3PTQ%WU|W2wGt2po!ao#GyJx7pSOgelPcon})Rad-*{F_b zt)nemv#B$p?#U%pN3u0echI0N*6p)596TA4EbU~DpZOY>D5z+}3CebqJ?GJqr!0MJ z6Cn})AU@Om*Ak-JdQ%ZZtKkIe2|d*c7>diA5#;Wi-a5*%;^7=olHihEwvc}&tU$(E zhE?QFO(d`*2|LLXUyh2q$q3u{z9bm^4cQHYGFm{Xn)3$>o>2z`;;^xHAdt0z(Uhc2 z5dssizz`?;2+0G^X zoM!moP%RpXi<_$6Gj2ohiO}SBa&~Ir#-Xalb`6_`ffhHHhKn%34^1P(c(mI zS)VjlaDGk{4vmhQD`rA!M3SuNpk6pDY|R6&A_af$UnhtUn9{fsX0BSdXChZ2Ac(5q zLK9|;7HXaez`a!CP~}gpifji=lo5~;$b1NhYjyCuG$Y+=nsp$hJ1@V-xW&VN z1}^RU*%^`5S(q1{ttk<%`%YR+qrKpzX_>pSbPXtol_|+sV3^R&c9qRBJ8|Q3NwLU<}qw4}uH(3jG7%HEjkfnJvCkeGXN&UV`71ar^B@9Cy zYt_@6VHuO;J}RMpHqGT0hq{a#P?D9_u@>a^k#pX{W?M`a@N6jcQk{mXVg>2aB_~f` zr@Lf0NX!Wzqfl13mpus@hpx-bNF1_uP~M_SXo|?-q++=^oQ)!NwF1U(&W;X9Q4*f1 zBgif)Ku{t*eu1gu7qtD0P^&U_#iHaIl)0eDxhqs|$d`2BkA+=EC3qUH+J%-^wVMhh zSl4DwNxhktEcTfw?7Q{Yq1F{#bsp>BZSO|;B1?I4FWn5W&tE%!;HpY-qI^ z<6m=~=bIyny7ra2zTqvQ%XhMj+P;AYO75BryY(;Sc*3jUY65O-%PWAW?ohH;rp!0T z&r4}|)rkcO_>Elb%F+7@v)&yX{<24-9Mv1=aXK%J{=>Y2l5mKntHx!;f>9v3Yen~j z8_IN~Tqr3S9DY#tuvE_gz?4W1iA@8|*dd%IhS4SCK|@h6$vR8DICxZ}plBCb6AKU% zLBJxB5MWqLe86n#cUOF+_maWk~44 zgNn2d4-11P!NAh-;j)sXPC~+t4wW7-TG&wDIKC7>U@+L|^R^2oWUs?1!U5sv&{%Hp zx!RATEZ`^;KrAZ!P1GdUNN`*h2r48f>*S@4vw>hh5NI+qZV?O&M}!`P3!4Cg;-RvW zgM%=TsEkl#WC{}-I#yw^%jpvI)LbAfEWjx!tV(&`Z<37(nuAS2$nwDSigqX#utc<| zP8+Hdj8zw#^KQ@gIs~DKAf!ur8x2iF*Uq3BxhxG_urtS>Q72r`Jwo{Qzb#EdqNzfn zpoK7jZ;Cb~_^89c$yOhF-JK@#O1GwG60fV=kvnIwx&O>)f`fs(Y#q06h(OBCA8 znR>Zcf#1rP-ilQ&W^%-kky}G+X?%%IYmD~{84-?!%j0(Rwyo-eOIHNTO*j$L!8HJwNQ!MEgAfrJ#krE$b#rI9i ziL%^EOduG{Vax7{MNSvWj3Xk#W0 z?|x56N_P1xoDEBS^||TVap>(=V;mfu(2SiJt-qprS}otB8D4TuojUj~kt1txBD%Vw z#SVbJuKA4Q9MM9#jfEbdgv0~lp%8Sb@R*pmNFrU(`O*=svO% z=E)dA;g$Icw6yVOg0HM;0>s^Mg95414=|jA>^5{shC_$kGBRa%Tufx4slbU!Lo2?M z;zx52DVW2y38^ZPtE$nHRiPE?8&>H;I;OCj1jX9i^RBC#jAVUDQiA`*P}6>PMa;+( zZ0_Yr>`V}+B+S(k?9TCY{G~Fn4+t3Vq|SAKj@%Dlu{y>2g}lhkVQ_05-)kwO>Y zkV?jc>3@kT|1KKi0>9zQ)i5Vk$`hC%LZ|&DHNS+pc-OpqVIrO-qE+FikFvCRqMmRDLu-kzpYvmz6&Bn2Phk*Ge5G)578>0W)fYK?Olk zwnqvKg-(PA69@xG%Z!EsEb<}{P@Iq~gs^Q}!UQA(NYfe~Jd8wftHeblMG4;~rDmFZ z&Cy}ZUmDoJwsOuRp+*e#`}gqSoL=7^4Bzvnxz!q7HD?De`89r&)UQ5N@51J?ZyKAhDjuiTd7TUKTZwF(#67jh$p> zD7*;XbRX$D5{BGpRyHO-5)Klyp}UM+tkGfe;)9PkjPM}fz*Un*pYhf-Fg^$eK?Cm( zh@xC3Ws8SSZ{WwW?3y|#HbRuyC8!(Bh(b!GnW$RSL3AZW#*Rx$u}`6lCWFyxr6i&> zLA9`g#}hP=snK3nTRHyng>BztR8(vN2~5KW+tKVQmdEF_m8Kn0a3mfYB0w-@bY$rp z3;7@!jbbKs&_EqJG|;r6kV3}Yd3*h#8Cz)>pK^?gLO(m|=rqKsXl` z^dxiqCTcF#m2Od+k_(^+0fXSVmM4v?fvFH^mf?0{wSmyVa54cP=%FIRh(1}!uqsB- zs7{P*;j!>G1kh|mo&WLw>VPYNGJqDq0k8b8{l{n~%s54Rl|5Y=8c$aZrhG^-e(K;* z>1AG;!X7qF&}C)jrFhCgdc^l(S|*JB^m^KLN7ROfjZgijc6zfao7r2W8*e2ez5BD5 z$t?SW!FJl!y8F#Ce`O_*Ta$#iFU#)#PH^Uu$vj!K8g~{8Ewb0?YHdMEY>tLjDLKi& zm+^^4nQ^0C0s<-S`Yly2q}Ru(v5lc9NP9I!|hmJ?7T zuB;iM<0K4Ht-W$>%8uSjASBicg=9QMD5O;$H&C*5q~NbVd&Ji!_o*+9fk zj@a)#Fy?6^ep}EhKsmHB2>bIHcbGh5q*jok*(AeTI7!?>%MIZh+M1MB`HN=~(ekPh zS>pB7%L#ouyog0@q@%>087Et>1ecUA5*JwXD?^l>8fPo&DFFdnQP_-=$pOzwQ4FeZ z96ZMm6p8mR5@$vUe8&l%SQC|AWp^p$h8+0?4!uj$Ez!4cjS?8XT0awL9}`jH)8DVT zT|}^v{Jpu(rS<-*zfNQ^VW5Jo1vMds^;4MygIr9&xa7ag7omSFX zo+_>?rl66Bqtp~6LcFA_tTTs$f;%JkR%)NgoVDO)41y(52+0IUwyro_eNZkBL`E~Q zO6zr5K~sx}xN0&NC7OyCS){=*G4(hvJrg*-JSg@`is7|X$X4Z|J?=>8al7S;n{3#e zPl4M^)O6JZ5^dha><0gfg*=;;)UUR&GAt-D+-BE4hEjGCIRMS-@;amT{YDHMxJ4xl z2oq=?8YSp6CTEd}XtaRJm?&D7{Q0?aSHh6pi4?3hR$L@6ZlyW7VY~H`fr}e~3cs&e z>nSNg{!)I+@fwdL6|_sxC4Vy;p5(e`i$yQ;_Gwd$#tk=>m1Og8KqXW4DWhBf=GqKa_70PEm%MWk)yO`y`PbwLcWj}6Zd5d_f zAyfO%=*O&9?tf9E#g>ldauH}<;y4#azFb`8L$BDah^v9vM-}H)U87wkA;UW!jYs!* zIxg7Rl^{ayU2JJ1NL-c+{PIdr_Zs@#Q{0j^rcc2A#6%g87itLj;Xtp#i6S5M8jmpRwxHV*_S z6V)s<(7Bb4Rh?`MP4XEOS#?xLUB6cQS3+&1Vj9hCD>CY>*%+#b=$VSMr@GS4`ReAs{$mnpS52 z9b?x7@3NXAXHsOXo(U0Jy?IGeQ%TDW z^p_KGX!BN=jC9IQh(0-MWWcu(!iM|=%-Lh1@tM`u{3wj_7Ob!2BcY*dV?gb?2f`OA z2%h8!n52~m9*Kvi<%dpDh{Wv54@6>PV{u@ikq*<=MQ5x9!^AQ%tJNzuL@m>b2*#lr zt6}?X3zS}#vmKh53)cxo3N?F7DG)gjjs*q|i;0_|eZ1+>8JQRtv1CT~B4G|`3L=vO z6GfR$i~H{pUl|?~fMGJKH zDY2$#(_=M{&l^1SPTPS>hh~dc9~`M=L_*&!qhvkbyZmOBkMcgbkZeMuLSBX`a}uJ< zWZl8b_KwJeqy%O!3dxEMj*=7;9F2?^hz4bd%K~C%l5-bhVPexGl5z%xgsd^yh=~fx z3LLI}&5?Q>Vqsx1(3q9M87Tt7Co0U$#NtJ3glQdFut|@Zxg+s2l>|%me?z%4s?yfx z$nrHW(Oi4Hjpt!XW2ETGSop9+or+uW30SeJ`y@Onoj7{nKz3{}SiqGQV&^Ot9tznG zMo#h=HPD@riZx@VgaZehsT$%TTHP^mvcmGy&96*7@MeeIIRPz$!LDAS9Q!zFd$sY` z10nTp2Gm^kdMvJlLj8onxeMWqm|j5>66!i&)>i_Xhl%7Uxiw!8mhwK8Nn)f+7Njtc z()BNzW?2KEinM~_F^=hKvBfAcWX?Yhq$zZ#oVIhE55f?kulV6C}Ju6%;6Z-lMMI7ef$< zVxe>vLbv#d1c`hE87vKyA%#UqiiU}YMwA6ZB)K?eabwCf4iYgQ2nz4kCsVK%OzD!b zGNL$ZPSRrGLiRypyh^uY^(+%SWZ0mATfwrG=Y-n~0!5CGAAKVtgwh%Vp%Wp{Xu2XI z=F>PBQ21<2n7Xc_N}&YpoIDyOgluv{F`ATxb+|&QKDZl*P!4~5pO;uoyI%`OIj5_2&)rp3jnSr15f4bAr)qum)| z)#!*sD2BPzEny7-H^iaF-;iGEGZ3h%pe!up5K7o0e&8fRTsr6^U4UGNW22D(nA~I; z5r%_e6NByvsTT;b69p9sg~WrQF|p8KWK1vzR2u`rA^^cr5o04~To_H(d+`IbLeEx7-x3m_#tbr|c;(Fvd*`pn5GFMEvxFo>m%h>P$cf;wlg*I4#aM|2 zr$>$i&^m3+M~sj>4TKH>z#zDYkb~sGAj9Mhsh?EyU7urB#d(8j^0Ml~akNY_XtBFTS6)t#f z^8wOCNj!O`!mJ_|Ize~AGL<31!ttQPZLA6f6P+TwJ}5>aDzIGYy$l4&R+bJBNJtVg zoLF(;WhBBa3=bt18WFq|O3tR$xhkmSNuWl^z`Y#^FFihFBfyZB4-ZuJOExVjomI}w zJZobK7?CEth&4v+tOxVIYhsTNpg#-u-2({>n-x)-7QNfg0 z2`Gv54h<(b9!RlLrc3B;Zp6++LT+$<{MO4S{VKH=$~JSmq3J8e@AR`(G9Wu6Kr10+ zzZ*H?418Wes(5UlS#t|tZ z!)m!OmAdZ|qK4&*Q*dXN;<=EHZHajrbOKSxfmj)yW*No!F`ekvwyV>Y$B57C1@SH? zg%W7VbLibGW-7%=FCsXSRRx(9mk=5D7EKwUg;KZ+DYzyg=r|7yf+29(vIc}75fdB_ zgQ0<7P!}K*22U3b1xHPRfGAKf8+sxLl#he00)gP54I2!A`kq4#RU9}GHqaX;un8#0 zET$pFg~t*M86rMLgjIegOb;6+$0RJ@qs`d6XE%6uIN*UcrKqxY&CctKD_+h|{b(ou zi!Xh7P~%L%y&Yv5d3JO~xHRbQ+c~q07X>s~bYbt>(GNe0GSw{5n_n!li|S* zT?G3wDw&lo6~0+gtD2V=rpA8VpLW&%HvdAkIIGMgdG0T!dojyCGBu|AY! z6Uwqs@5oVzqF0EXL8%gPktIKIJiElx_a-e<`{sSyni`Q>YBblHtL->kWVCmoYtHLy zrR0dmM~E_(ZZn?cT-tuc&C;~CsLhVjpFS?kw8LbZEqQ)wCY5I^<)Vi|VL=#?|C;hy zr2zsV66DfzyeO&`HyD+Yg<^L>D1oZfZA)ytrO6>d%v|)q zsaR+@R1}Mq7&XI|B<*zDv4b&jd(pF#(9LL&G$zqTaZznaSoHk=_{pdc{Z9X;|Iq)& z|F!?7|DFH$0H?GRt`4O2o*|Zd4X3n**R?gP=*^LO>H#86sw;jZlhKGwN|tV-lUxZH z^kBl7rmRw|pNb^mix=o8bbP!QFvfVStcZ_xGS0Ln!u8B})%=hN(P|-ZM7&E=&;1z# zAW{`L!Rj>CnXhylLvcDRb(EMbwGuTgB2TM+lf)V7*AOi>ui^ixIVf6wg=@HutJi-B z4+2B-bVGVPvZ}vCM$8bgYlFEyM+80z3wimfqH@Y|Nw1`&FQ&mIf3i{EpIRnP@7axA4 zA)Wqqp27s(acrNu9lyUwk#$_t?sZYUE1QD=dD7*-SfI%wdixGo4hS?DQp z>RDVgHXI10*0sn+x3nr51WkoPZkHq-iRWuc*WHi;V=Ueu7qxktHj`XfiiX@uyqv`< zA1TNz7&s(ZDG8n0h5oS2kSZ)??e7iWs7D}aN&!0wV{_64E{;1J97-zD$pI-jL7E^{?!v|ROjeP?7~dOlVFR+#q@Sf!6cd+) zk3dRv!qInE4VA+nMv^d$WL!=U2LUbrf^r!Nf~80e7b2`TH;_{ZcuiQL5xKiJCa?m* zLeapnE-OIPMv+58O&B1#DX3~*f#5(BA@m~rZgM?S(jnm$;e>;BpNrjKKovBRHYvie zo=F%FjSQ7efaJG=yaHy!145My!8y3E(@D>Oxlt#|?h3;)Z=ybfstl&zDB(#P3LyZTsA-5(ELglE|g4Qiu76%&Zvz+Aqg-T6n@_w%tS(E zB7@>_z@hC(o(Mf;FdK*gGq^~UiUF}gXiL9VBqppp4+6lt6t-Z~_acqk0o2lk4W3lM1ca5Ri!F;BUzn@9 zYM;Tcy922{Z+0QLkjXo&dUV-RQ9ngGF?T~>Rr+OnpULqNStBl94lKQLD|M#rweAkP z5zl*ABA`l5ufE%~rufNGT1}cT@S{L)yY1EjvAf)v_>*gS$mypLvF_k#v z#Sv}2W#%M)Cd1;_E^)bwYv}JPm$C&!L=*$~t0S*uipOHq{vA}6RU|d|3-YhTT=!M6-ePNwH+-xq;sJX!-)oZ*%cB^x}RN+4(MrT|2{kw zqD^9f!b1!_tS=O|&?UnaGm%_O^O>(8Sb|FE%TG1cx{w-C7@OuDxSCz`+@YYE%ados zetSG``vRwFNN`D03hz6pU@kC0=iL)_c15nSY*F%CA;w@PRU+O%(D8YF9!tV?R|1tU zTuduyJAW6GmU*{Y!iBPjrl;whnXS=k(u2aZ9>oNwmKfF+8;}T^5D3cOvwh{;%`8g^ zm`cauJ$~Oyg({>)%LNA_!xl4RBkH<~Rvfol8!vT%k@`eeshXZP4+L-L#{*1ksqb1Q z2}qT_{V+K*7ZzGdM5A#U&BulXq?r0U_-e3)Hjr7HD% zI;gH7Iw4EVu3=N318|GHP`;xrM(Z34!XB4|5@4m5X%6dReITIz)QafAB^+A00~P(2wuI4$gCI*z=b^*IavVY_yvM$S79VIIdH%`oFkn<9vdb zMg{5US@@%U@?NCa85t_i5zp^FQlg2kslu>~l!`&FwT40k+krqV3s#F~5XKSkqELr_ zIx(5cDbBhTrK1#@q~p+_Ms*p(SaVHSsF^I2JA_1n*jp$9VgyI2-rI(PLmBkS-qT*? zq-{*0cvsB$)rKvJxIY`b`bOkB_ucF}=w$+>BPmyy|z^r-V;Y^@bN9e%xc%_2K zVWc51@rF&;uZfWwlC+rI5!;-m5(r7f3dizlOzsje6H02FNUYDXex6)RQS7l(FGj ziVE{pVuRDW=|KIx=5MujrvxvW<)Sv5-wn$MjtttqSfjNShHQ=|_+T_Z}9 zRJqa#IfP@TYh5EKzzYyq*(+%1O_A82H}$VkY91`Kymcd_cS|-9M{$Ap-l^NE17w*Y zk9f$Fp!_7x7OWnN#uQMJF@+1Ebbcm3jYLGdq*%I)D?+jAPl^~h z%{p~At;B9YYbaFq{i)QfPO7HA*xeC$RxXu8P@s;kVP=79`386+|4V+Sdo<0&bAEM(hJd7serokxbV{cc40Zk@I zCyX=I+XO}5MyXClFTE0kR1!AE0TDWDs!C!Qv!tF0X)rX63d5RV^4Vxc-;$}migxGi z-%wQHZdqIEJ&LSi+>o@^K@0>afl_eW9F`kvHGUvXr&Oyhu&Qa!DZadj@4}(3I9A?> z7meepVeC1g5eglzuVFlpyaxACL=;<2`#?r53F*kw2Ce~3--!oV2v~kWvdw(kbyR;n zxBXWX(tyyo?!5e%_?~a44dslaEjU}78T5aLsCg{ndsn_@0@T2&&{j?{GoJN3nsW%i z&_XNP<{8n;S>MxVX-Cu%f)~+Llr?UwOL1(Cq`*WvU>#CoI+}uGDv1okFPvb^D>kT+ zf=453T)G=0$bL3_?{*aiX$fj&_1;)|1_3dwP52Wex~q7!`S%$}xw_AF#!p)n$SyCu zJ53ygfdoe#fr7&>>^*uexUq;pBXZi2AlETQdo%~k zn()Q!`hv_ljq8}z%t5$r70%#55r_ISgJ*(bmG%H zNnn}0fG!h34J=RyMCmCCiw8-eM%egv#gXHfmO>E*lNlOa5eUtAhQ==iM2wPjv@&mV ztPq83sJxansjCQJfE|e>G$NA|jXXo@I~KBQ?IQ%FrJ{(YcTNUnlQ_WvARB}b zOF0)ZWn+?G8p#t&s7cHZ$n-e`F}&b5QjS(kz&LjVHX*VSTzeYH`!hJ$Bvp$to?-~o zz&I;Ra*qIHv|sp8SdIT>ad&!EcLuEbVPwE5jJij`;j5B_;c&QVd7sIXK4Jmo6j11? z1ZnGLk%&PFWy9cD2}-T7F&rL3PiEx>PxaVGSAqjivsowfI>=#{fUryzm`D9fRip(6_mLNQiS~)e?)O7bPJ~lB6q`K{am7o5RS?G&+?HUXzxnTu*5c^6j z3IfkSm07#^wqz0*3IjQx;ZH$%ZLbaV?~;?rwy_?8Lz;-RtVYN&^azI`X(VCF?=Z~A z{@R&|Q5RVUdWQhC_#pIpel#1?Lg64lQU~)LvkRi3UuudmIO138YwVTh!7Yi{edHnume_xcq0_3`dLmi-bm@Q>bM*;gni*x#nZ$@h1EU zMP*{Zk-vyY3mqW45+!O-7eiV;N{>V@e|8W=6jDnWVTwTzwI)E!YHh?=dDHmbYbv*` zT8_|f6#k8;d}mexW3L19RExr)kiOJl9Q2WfvQ5;tYu)|{VGhZx!z)R+Tybm-5D?2o zj7jL42m9@TjKa{ymeHl`FpYA`Bwi4XjRz2b5dzy_5|DGDN3wOS$U5U>Go=-Y`b4w2 zDJ?Xph;$)92TaYNXdRZ=p(#ZZ#s>hpVwhbFQ~#|2E&y^Jd<&gJL-H6hCjd0SB%lwl z2p|NU0$c$Y08!csTL@B0*w4>L2H)62SAtXl<_bM~!qg!KH#lYCI75WyX7 z25QENhlqhqd5=hs3rdKf$pFx}Ba_DBV6m^P&i>|h6q6ff;B<*M=Xv^IA66Xy(hGOO zAB11*Op|0oT8NhhR0OMc$VlDjnG<8_i2@|8Wwl~Oni-OcxIl|?Wrpa4#HB;wn=#Pv z$%z_5_aTzecqS8MAXCs8Ot(Xl0A($di`@}3E=eFs_HlkdBs-A!Tzr8yAYVZE9E`(> zs0ok0Ciyte^&F_khKY7~RuHISN@eyX6cMj75i_;ZkoP}Xr%qF=;Cw}ZilF~)5|dm{ z;)ThU(YSu(s&J`O@sUyxnPXSZ#w`TmF;cX5DjOM45hDfSj)d;3_< zLV!@KhQz$=Lct4C?u4i&Zw>XB6gaTXZ5-xVS7}NFN9O&4a`%Wt04P{cWK!J;odzNj zkD3CCh9+h<#o3Yz2M9S)OlwB9)~U3u5z{s|PkUgargOg1$CGx}P1{r8<~b~4aX ziPsu@_?YY_caWf^J3Un$vHlUAq9<2hh0*0s-L4lGy)Yy#r0_$K*q3gl6BiF|*;UKl zNOmZ$K4&2iB9b2}#Zgo1#3Rs7#2xfhh?IT;m>(5|Ije71?4WqdUs#6Q+SflK<)@npSov zA89x>$#R3KMJz~U@1;kd|K+6VZ^)J+6xq^Abrg~d7L=8UwPud!{AYz^m|Xb2$nCwTxe=hO=5I^>@>|+SC@nH9 zNm)y4d8WN1ALq^#)<}Nzn^aglWaYCidp70LnQH?qr)}jmAHKV^jYmcQiUxH0zCAFf z3s593D(B*x*IzZ_q2qKV?|1ze)6bRChNxD4X+ZAo-O`x@W0S>bi6!&|<%3o0mdNIT z3H|jZh?szkC0CNfRa3}@n9*Rdu_ofe1K8g+I!jdBN3WLRtMiQm)m!WhVu6tY*_`6= zm`#d+us$+pQr@EiSJ?gt*C7^|m369=TyVDx0@D^oAWB>l}XKM|P}^R{{gYHSjdYj2bxRk^6|8A0|R*A++9(%~-YQ0uUSUvy$2 zh>=8+XLNUB`(7oUTHfEWpsr&3=5wUs%P>?;<^3`JcNqI^Ht1jQsPWVrtDm@nHvywz+=i6y_ClVJi-L?{r| zO5{5nTa24X&P5e1VGdpqUYVE@jLv$97O%9p!|W_4afxs7DEF^0_J&d_1*C_U&)kc| zKDHG|MbADg#ME0ytSiQLkSZ1>xGo@93b|ewSF=|#cIQCeBBPe(vKfcY(rz~lT|pPC zTIgIMLT*{9ps&p=GR^4x9}qf{3Bu(NV`H|Ka1*OvnqJ%`l*ns7*++rtl~_c8IYQE8562;%a;VGHfZyJk^hrYsrEf^6Xt1%qXlCroQ@PQF) z(44~^EG3{wB#W9Rg+W;gaR(-ivqfEDxL6Up6;QAkV~gPhF!&-CHLZSagXR*y@QP0x z1VaU>;$tWB2tg3!=o%m~h{wW^z{?64Wi-mTDiECu$K54D>5RU4SZH_@v77;kjYrw0 zbFOggle|zXtGgX$9Kf1%IdwiE6o9Fz#O4-ne!@WGROqsrj!}Sp59$d-AgL{&G*FCT z3Ux~4Q7aWd`k$3}Q3xafL6E#l z7gegN7mI4SD$qNLr7XoXO<&bYvN$^f&}?TruE1z`fcF7ITHMQ2+)B2RFXbZX4uE(Q zvM-4)dWi+)MA);Q{K83DbyX`9q-9g&G%pd1%x|i-#zNz2NvxVGRu|P-S>BS8Dm=(w zLrfo=FTR}SmV(D%`Yf|NLvbSMB=ZQwr?fS)T0oFCIy1kN_>KlC^3rJ&npspw8U+jQ z2JWzsX`2-a&jV?klv)G_#~4}DZV#ukHJ$-ETgzKXc~X1wc62ScPz9iw77n9oBtH0q-%N3W+3D7~XdgS?tcn`9}jSAZzB~wZ@ zCBgza2Ed;ju#%)&&}No*m{S!<50ma8kzCj7ZACpCqVSq3u8Kwl1OwBt41MpbK}eal zWML)lZ`46J#Qcfp8X5~S72?4wCpFsW*W$Pk8OW>Xu`v;cry(_0gIY^A4D{TENsY7U zA<>D#nHKG+lP64Af+n1KEh5%9K~K;|Zq`rXT*lOHPgO;?p|wAPy1vByL~p#A8C%x{ zK&<8#<5t#EUKC09ty6V(?`E>Xa}y*x`?3A>E(+h8Y+h%9V3*&uswL6B+EKt(mEX41 zl2wapLy%}vH?`8s2}79-pM7hY?!RTggt*s2VQiFok?@ot4h9g))pYjVk)C^-3J6Jr z8KXZBiKytxsrM6%4+cQE-XyhEHtS($noCpE-vpq~ffF&@Is`EJ%V@ z5-SUVpcX+yKx$i6#*3c&96&b?&Y6)hroQ8k= ze#L&bQeLx?7jl8ynjmU2I)>N$gNReLHevBRbyQIL(=}VAL$&ceAU0yA>^W``#5rbg zMN6^d&~OD&RB$L+{EoBjXe2O;A)wS{69Fk{yakY3f^#OiYv3D~!7pi<0ao3vyMng| z{syfRTnx)cur8hYE%#1GRz4_+?M@dI5-G%BSeF5)a9+4lDqj-8T(E@d}3 zH`pb&hsWhuuB6~tH(nvVlAJ>7rSOhNY$_#PEHESZ_A0WC!2Dw44xBC5j5W=rm*r>S24RD7*|%$y^3{VSp-4H1+m|6M=FrcI60V zf8|9`>s)sDX>bjY*Zc}1SSkZ#_4-mM3&En0hsEO2XXzT)b72VxhV{W2?R7}TsuKdI zrZ@ssxq$qX7H+8!MfMQjDIeNPmCLc(9mLXN!f1E~OOkZ#l%_2VK%c}kJ&&kQPs9`4 z%fD!Fg0Dbi+ei+@2Am!n2iormX zz)X;~=0|f<5}_6FawyH=n9L!As*YXeOXNR<0F++VH7*63^u>S1YH&35i{>zd^m79E zLAr!`N_U<>9G@wihybEjXc)=o$LuTgRU6sKi-Ql1yq z5D3LGYh!tgMvQL}!B+^qNRjZev?UOkn$XpSmRByb&4tRxvV1i4LUWs`e4aW5s|^%i z{tyV7xRDZ*UoI2=5Fx%2K7rojl&ewui##$M>Q>fRCs!UDBgv8(3oXJP*>JmiTm@%Yy zpS=ahojW@1)a56{NhWGrax%szYkFP?Hv|P5Xw`6|%BibP9Uc-Vu?x?}`=fQnng{bs;aQ;!%EkLl?;PAkdExM0fSa>Fpk&+%qyfS1 z6|v@;Q^O%OZSw3AQdWSzA4ecerSGmDBG4`w!7L#@3;8DuIn};;X}=au1JIan0X+6% z|MwmwuZqXZhUwNQ#)lDTZG9Ft9>$ni z@GKuGEsDZ9yJjx2d4-pHPPcA2fu}eK(E~k`e{X|yJV!fHu~XrF?Hto-5ReM*Ow>w^ z1ujgtl*gKey5{ef``kY4O-wkX)4}nmCE&YDp|a?I)H0glE}LlA36Sza3Eqs3Qo~|5k!Q5Q3KP`ayG~ zxSqC5E!6|ToDE5SME?*YFGF*m2O|&ytE5W4=ZuFmX@Z< zX?}mkSCO}fL>{O0jzy*-;o4Q$tYhbo9k-+k@R+TE{6r;@yZS5Da`CFBY$wu7$h?Hh zj^$5Td?aQGib3~2!xr;F`*~9^+PzdI!rCX!@>O0Ek4q2=oEll8mFH8?60v?3Cg-$mgxGuE^mV+H z&cJYHDj+{Db;TK#`#-RLR1wC-K(}T@xka)GL?NRU40BU68jM`1ajK)FjK-=6lmba} za6?_akqpzM+9I72i!{>H0h`vNQRIq&MRQWwl9{d32uIP`rX)oc-ao4ScPyKM`B4!e zrI;Mmi`D{f&$)x3W<&pxHgJ4McP`f-n3A-@Tda7qES%y&$h(kU{oRMQcvJ zY06e2IiZ&o-Cl~N(Sr!OP*TsSX~8_UOx%)2;@Wt^(&ba414xuw75yeVm?WG^86}RJ zEEtKt&e^be(k^%~t4LRT`OvVgis^?PHd&}pgh$WpO2(PzizGR&$VBq}>M>ZAHc3rl z%q_#15oHV3v_E>tb5}PczN&M(5LZJbLv|@s^+K=13D;qbOx?kj!Yd zyBcyH*(~n9LV#Or42zL-T-M^Fy#7x}nHfXVz>PNtqH9}HQrc>Plg@xrc1v{GhT_jV zFLpg=el$%y1ZW%Eakwr|Rmyrs$}SZQc^eXS$z4fBgENW~M)$$%L8V`!HduHXRZqrk zyy{GcnznR0eWJ(hAM24>(3#_kcCIPRi9gK)Mkd@XiijVh)xE^ju9FV#3BoPqm_IV?& z5i3AQQbdV5)_7?cBHv5wh`!)6Z9ykk%R_gQj50FbGUR50hMwLAJ0u7n$^1@9s3LbK zT$G_tNtBvV-|s#si%b{vRubQM$?)QDPW;o^fi#aQl{pd>NNp|a_Ig3U{mq3D+%C>g zu1zT5`0pw^ey!Y6IA=z z;CMo8{IE)ux1ofm#*O4^l+pAXmlBpL+4!z0_mbNP_`Nk~)IgtSQj0$;F9a(-aDiI0l ztrh+L$guGdj#5=T1aJSBm+oW5|u@iuMAXz8jK!cG3gJYIxwh3fPPu%7!s-w#d$LI6BW~K zIK$RwDiR5O%V3k2@*928K{yh`)8mo?*YR(8Vc^zXHVuykZ%C|<6gySZEbj$~!qT9x z29Y=TBZJ9m(Bs01JqiRdNT^mLU`G6;A~KvqAQPOULguViw6h%)uI3}2@O;@7Z(3=` zTspmRsv;TC@izsmUdN41qT#|;RZ+2V6xVq$t$@56xud^gt8NPt&mMnh8Ff6 zZcr7=?koc!nUjE^Ol%Lr66O;bKsQJ*NeU>5P5fBi52n;!SO+$ecqEqx!hF0TSD>Nt zWQ$U*6^2Kw%!1LNyeV8ocV9FbpfV6iVk0@kURxIV53Q@iOP*s{A&G}lIc59fgAk{n z;nZ*jDg@xD^;;o`oNXL>0^s3>`6PW*=<4XQ8FjI{+6g3gB(jiBL8Ay+Bn}^0UWLL1 zaFiUP(mNCg50M+(eGwbHO;sbv6&X#U@hlJPo&(K3NOz<(5ghb8GxmO4~ixO9rW}sDe(aeb=ZHJDhuW^fyIoVsJND6(YmF^6=Ei~10rKax#Y-u3sfhm z%n21wJhaDnZ`%}6{y_}r+lejK&o0hcaow7qFlJs@Dlq5^&&pk&UI)YRScc!AeOLi& zxZ8Qfmed#n2)@XRBtRR+5$PEm^kS4@H~jS_St?c%{YLvr z%meFR@PdM-8P%HynO}*o3=j9wRn?EfcLFWv<{_xbNd+XJHdZg5R&uhFih`Q1v35++ zU1Z*_P!m^1;@&VgU@Vf9v8c&fN5~YX?hD*VCPxU?dKtTE{Z?JJHn1#@K4&j7mHsHC zgajn>@&tnsOSLX83yT7kjnM-c!E`NJ5YUW2jo_FYFE2OTQ=+t&HO5 zWTK9Ml-Qi&#y}YoiT>R`i0$Q9^)=f8P+Wq2IJCh?m^y~ryP44fr6Cugi#F{@K4_|X zXR6V|L*Z!ksUIFc6T_&ktv=Zy!6K^K>7Av^=!(DY>Zbz$wL)Lz1m&jfXI;2k#lVDL zb6ArALM$hY@Shb)>AKYzHx?s2<6}0!pnEjovmAs#Q=ZWRpo~nP*Dep2cM&EW>kA1J z-Wh)3_>IuC*zt!8>sr{|!sQqlZljz{2SQ@uSy2W) z1luUr-O1{{Ic1ZJGLgvJGG)mlVFgv>*1ej*#)cKlF@16rB$|%~>i39+#SaU?| z9deqTe4UEZ8CmWSPAm;U1TOV95gpTm*LL#8bk@a~MX3I)O$MPUOlmV=1p`!Ijd9&$ z!y%C{v*^kZ$HfGPK+mh{8^B`-H*LK^=ggpknwz z(s(ZyhlhpS#R=*Ct_dU;uh>q@k7)&Lu%!&8v=EXky)iQPUQW=ojCUAN51SjyWFbL~ zSQ)I4#H|fDKX9(L*Aw=#xt7@!XDZkf})EUBySo|l!-E#(A=Qx(w1!b0C4%eN!XRpp9fp{uzKnFphh)9 zfh9(&tBq{!yl*C4Ml~;mwiEf@w|D!{_dHuB`y?t&r*}ci2{eRan?*3>K+bGhIo=Qx z-fYiQ;o1al?UIXL7gg&dxYf0hDjtX})-FDSIKf3Zn_({9zS23hFS) z&`oHS5AiOt=z}+@rxFqMZ{smh*~5bzdP=O>4YgjGi54yJxZzZj<2Wa|!8zGl_qBJP z;`l7%^s16uA5A)G6ES&%IuA$-N?UbSEZih7%_JWMLlPt+&cX8e#a{WqM)*jOd++Nt zHa(@#uYCxvTV?o2Vd`RI39q=mPb!HfcOf~o{$YOM^H#@>mdvYYl4;PaZYEMQ6S*%l zy`^?QhN2}3&<@T?WQ9$2m&|2xUfoaZN#mM_P9#iaws4L^<|61?dHrC5tq#faDoKhT zJPdn}3q!!Ypuk#m5iLESQyrR~gYXURhl=jUe?-5&s0Rg3HZFe`oN02QMMa&Vo_lbRGv``K{-#sQQ*Y&C7rz! z=3@1$F_UJwDU(&*?df2oj5QNCx8ak*TS@kojIF?||GE zn}jE^ghrg^CQcvMjLxt9osf2-J-oapf0@6#|M|cPAOy4l7yvi_^x6tj2@&GrkDo|} zL*PSUSSlS?$7B(0T8OVVG6&GQgeiJkf}s1vE*}_EV&_?_4UK+-as$e$ zwe13SQ1Y5kHbtY7A`gI=F&jn`wtz<{`axqJYTS322*1F$lZF ziMYzoOK;;rp#CHYLK;HAPlqr~!b%vM26DZUd}&VWhh}U|4O{MvhzTNd7e0z5K9HVM z2I8_SIkZ>IK>I}?r16%a*;c8W!mC4;LTiuBNcas?@4g~~iW$lzx>5_>AsLk(R#%vt zT7tp!Hjj(O+C}MQ6kx19gSeRJO>~F+RDAK$63g1t5v1rk177eVHjvMo9#1Bq#2!K@ zsM3y9Yp5KxBhW}7k8Q}q==3`Uwt)CY%7t}AzHS7z&BZ7{BdUz;!CpX2KxbVyn49 z5a9&stptZ@%;Kyd9TY#^-w3G`+ApJy69Av|P~WMLh){8DU`OjD*5ZKlA!19(cX494 zst7T|!gw#(a*I2ljL@fIexJ_gTAkMiX9_`#&s9uNT}x}^L@E;Mho6ob<6M=7wGS%zguy;6lsNi@P3rcQJi zS>2YJjF2IdliKexn@K0#<-r(M#OuM~EAgyp68E1h8*?jY30At8rXK2)NoFxufY`)9 z^=ZT9r~*j-Bh>{2A-_`Qpz!pPaFQXqq~TQ(e;G6y0y5MrD9C5`^O`thBo^j)QW%R1 zIFSgUewi_hQR6|t#21an6T7D}I#{nDV6g&{UxBdgEUsQi1bKpBMJ@VYcpr>~W0~CL zk!G`BJs%Iyh~$D(Vd+HykSqN;3~8jpT#!faGa2meP#zM3~+`wP{c$?kZUXesE9bwIuXZ{;J6e_ZARRI`^Q{20;NK} zQC!v*6(u4IeE{`HMNpFFhRMhwme&uk0ct&yB8?-q(o7VPiKu<4skG(6ima3%k+ElmUxHF<8j)QPyd_CQDo9AeNxIkQBs7rOo52|X+tOg8axq6fdXCE)FXRxABO5)N*w+0N zG|Gnz>2SX%hqN`kd0K7SwX;#cwKSA&F`Z&Akx&kmlwz@ZL0_%$g@l&oi}nS`3j;#K zGvc;0+inqlSYD-`>S@_{MU^{lpo%o?bMj(AY#E<~=+Z{QsQ%1NDU+kOcrb{?gIO}+ zPcZI0vnQduA@U0kO%qrGLYRt}(owga=}UV9w{;EW8Es~pWEa(_jV(jh3W6cpN;DAU?rbjfal~htOITetvPv3NprK{M76sbCisEzf|I~6*=ve|Q8O;ymmuyUkr zXqqdfs$5YO>J~cTe9%%x^kSr4ar=9iPn?~2R$M;O&bFQR$}zRI+9WI!$qpJ17n;)h z;+`DHu2Y2p8AVl30wS$pX#|dm4hm+(4ax`1%sA>`e;9PiRa6qAzQa(era&NTuBqnP z0g@F9*qgpF5j~vb!n^0IqDHo@!mWXmwQgMy&8&qa9vY{VH%Sl}OQg>T)`ytA4s8PB zUf$$Edeptmdypg<2xAz4t<+*(lRZ7rNL8xI{{P5?RFPO-VlZ;gxh~#}dG6ix_;Mi} z?YKsnAgYd4l*na*5v{es0z^BD|D`PRZ@Js(#42?_v6bm1&$vlJEoHcnLDHB?HdSHL zmcjd8EOm#Xc54{_bhOy2Dbt4*TdGD8mw{uaV>`Rn$EHF%JI6vrUn!BE2kYE)-}Y59 zscBKzw3wNKkJ)7GmuJ_MmYjHz&`5k{`L{7_zRbRRWGE|W)G=u_grX#~0xK(_HgM=# z6QiMnkdSY{iswM5LHXuc2*7@KFrBI8JYZ34b=b&M7O)}-SgF7&z0crGkfRFS1q44y zL~%xj>~?iwMNORDYKX+>Ahf5~ddgczrNOvhqGo*fLA1(AfNk}!$pZp|*lup`}tb5mVzqe-FQ zVH<++1t2sD$)jq=fK@~YL^6CCAB3IZ8%QYsmg+5tEYHcrs0a8Cib)X?eZlprW}~Ac z(6GUUyhE~N(c4&Q)@aSItwjM`Lqrrs8xTm3#C~~K**P{JmFtCRf6*%ugvMe5gt^cp zr|Ui=2xXNx)Ci~sW*3%sIc+k}m|xJQQk_JsAQ22vQ|&3(GcOnoUbZN-$WO*0Ep$T= z`_fgrh^+V^nEWy0Fj&O2mpz*UaJ_?_#-c2#Apu#^tC>Kf1QNI!*A7 z?prR&JsFD(_Lh(!t&Y$ujTO-SG6V{x(SdS^CT|aNqvjQ(;lWh-VC(tJ6J#i6ju2GE zs6CF(qFOSkSPOMB;_eRf7q?q;)e;Bo`(p( z$-Cnp;?rB7@*U3*_A%_cz0e&+_GXXAJnTb)STE&@1`T=>W9 z0ZpEaQAPJ?_9LSspqnle*qtTz?Li8rGt}MEk}C{nHk8}JSzw;{@W=8&revE>%0+{MdJ&QuLSD9GSnk9XTfu|DJ_-IEP>Sgg z9r3G=%a2w+OeD}7sxF7NIzp?)O0Y&_yaBh(knHHGqg zQO=2SAvvMnge5m`iVBbMo)p@NmqGxcJhj2?4AZ9oSw~DMw+oG^B~jO+sr_Lr>qDL! zs^mj8GnGddFOx|PFQ>;%4a)^YD5fD%YAcMyV)_9vvQH)$hy?dk!l=UmWu&e)x(*Gh z2Ph&y^{p62d|!+rF;&2n+oo2eMCzytNAe+=UcC zgMdVQCpt-RcB4^ou-G9QJU1M@mQYb_E<))!&D8c)RWOlSjo%zpT)K#856%XlkO+pUpx6(^8|Q>)%uKP9 zPqI|W^AX?K+kA+j7hbT6uQ;A-)m+!OxVBwKcd4T~24b(Fh++r>w#vjnlrKh%^nfJ; zL;%-xXnuJizYGi`%f9i%j-^n${uem2{CH1K{#lUyXt4DNWMHbr3xH71P~7h{_-&{Y zq#+tV7cMW+OL85n>&!5fsO!~sFtdbM$MPjVlYl>U+?EH z)<29LdI*b2?<6PYReNvg#w?tYOs2t_FHecwE%W|q7hh_uXyWccQHDY)6iRmAU-_2& z$=XgjR$!6tvbD!kdczSmz8RK;r%G!Kl2*xS&mLhdyTjvfYDq_oD3;L^Yp2G3S=4M>hrcgs=RnkITg7z z7!TRkOwWRgX~1Y8H@c=Q3Ide2G#h2c$Q`xSG-FGqTR}A@VK4h85JP-g5(-Xoqq!jdxlPLXl{IoL(wcUJC(P(Dvq1 z{x8R|l||RgMR&23^SpU&6tawY^5PtaVY4C|LbqWmgfc==AR~S1%#h{UvjEl{NC{ly zKhKacuMJIoi-cZSA5=M-z{!pO_{pde^G^TJ|CRp|{+j-{{>c55{B*Pv#xtTV*&aTp z3wO%~-nJNzflw?dIVewHjySZn;VCN<1_TXGlW_ltJb#xolH zL?HlyOxzp^h-H+Qu>K*th1g?+*#ZJ$GY2?pO~{$rRQa3*$AYnOeGN*JY%0P@w?cY( zSwVmjhlS;Yv|0(Jv&qk0FGmKe9u^p>wy2EY#&dlw*@Mx!F*(qXNQ*>>z7fr1OR2$H z?dyl(tF*K<*_u&sU!YWY2t%i`?wx+;ctHs#+A*JwPadS$5sV+Nn8^QDS@`+Znn)KS zE#-HTP=F+bk^gO61uT&iub~CSN7WwtYvH4FV^j%<}Nd25+gzJ^qfKvPh$u{+aX|br8rE0 zRbOYZC1(Qi;VT^zQ9%4CbttK}(V4*#?%?c5rpb(kkfC3gk}06LrRac+rML4jZZfm( zT>@(#R@Yy#KpdcoEyRh=39dITk{bhiXuz3TrBV={1HV^-4jl++^;C~XMX$L>0G;_e zAzwLhyDvF}!4V*^83>E^CRN?qP7n8SbJNGW!1%me zRbilH5RNC3$C1qKSPv|_4yK^>D)!I0v}(LCsGqUD`+wNV2&W)7Ar%S5c!DGHK`!aL zeX)s?LV`TR#&%o$k4gj}&DhJnZG;O=iq0i`9-PJ=3_=M_QJKP$8bk^`oJ)8d*4{dh z)UcC$=H($q{ywU&_op8Em!mD@SXyK*K?ovQv?%I)8|8%rGP_cSQS4!IAaPhA6^b6s zKvra$WQ%~6MaW`yaJbqa1m?fuIw&`=TNI{!nJ#8C~O3z zM2ceDj^6Q!VoDpnQCd+(8tdJ?c$UHKjQR6=UR;)kpk$`%Q#qf4u&3T=qO4Lr$5?r^ zW*~6FW?T*{;c=TRnug+LBZuczq5FXs8Q|n*8aE5{&%<%J$$AtB3S;QDuG%823=L@_ zRm2vELW%6wS1rOWAom`jkV8RGEt=ugOKo>emL_gpwK8hrIsGME##brL?N@6dgV!O! zh^n)Ej+)EIl!r*YQ79)hKKt#b*o-hkLOw|MHMos+ z4=){l{*$kCvvW_4lq>j6uQMsR-cEyv8=5?+l>&Ca$a2Fd=v}JW$E1m9Y$Q z^Tcy&A}4Ypc7+45nW>Fb<|VW_R9wqtiQ~0A55s8t6f}l6tn`LVDglxb68KLY2Y}xH zw_tElgaoDM;9ex9n7x~e19=Fjb9g8(?3I|To|=Ithmn>f_q0-famcLPl)kFoHIUCC z?CyuYyavRwyZ4853TAl8Ya|KAJhz@+*9{$4Q4%RRpGzaTlqUuP^Aa2Zk!wpE-b2pjS+prLq^n*q3mYL`Q6d5q?c!4D_4(XWEM|XBZY5_5e+J6voc)4h zXc|Is$W=HKysIO@S|hj$2D#(X5)$LnhLnpz6&@g4zHDOfNkJkw#0%5c_`mSp6Z~p5 z_+nye3}T<>mO$v&oItnJac_ZXCc^S;4U+c?(TF6$OPVlZ^p9V0aR8!MU%uc{qJJv* zwt~5poe_MdwTWibSrPKsTIEt$fG$tEM7WceCljjR@%ee-cp+35Yy>dIE_g!?6h(B) z=@SEL)6guyes3Bkiz9(Ms7$gu_f;tw3$gbU&4CksL_~auj9SH4R8da2)GKu7;Pkki zS={RDnbAdl_a{Qd3Hi;v^-h59D#BjE+-AqzmBhb7hFI;t+7`$xNY{mP`Uq5CmpNoi>NR#Kpb5`E7*mkYREIU0p)CUfh4B#oclQwm+u`>V=!X7J2N%GQnT zUXpDg7-bVp2XM|@JWA>}p3(wvtRRizqJaNQ<&gx7C3(vDLC z(gb%kYJ-g-B7{AckqTjbCCSPOw4)}_%xWX!aA7TtAx!od&L$2BWUO*e(pT9zr?R!6 zCL`ZPfFLhs;&);lkR2ombdd;iCrQ|bghnW=pcLnyquW?Vf+`A%m*J!%p6%B2M^=a) zPlP71Fl-p{1!FqMpvFyh{6z)%K#eL8po+vH4dk*UBbGDRnW(ZCpDKl93g*Zzh5;r> zz-#W&0TPGNK&9OpyYFa)Ie61@$57eh#iKu^6=}50M_!GP0&cXJ3Ex zwUAsHB{>u(T*G2euhe$h%B^sa5+x=V8S%m*_x_exTNizAQzCOz9}0CuBm5^o=7#)b zoj8t4Li_~F!(HN-hM2mm`cOm~stQaABb3l#ygPt(UKpbe2P}aQQW%+mjyO;btsA-#%!#g0gQ@UamtaMoWC#<@5Z zRFRNL4M<`rK7`&adJ#e9$-qyr7ZgZuis5hS7V=9R4g%OFt{V*LfMPo(imc?K*Ggrw ze0T|5y$Hf$Lj+53#*-L?xWFEqi2M?j0^nY(Rhku?2&ja_5eO0_JIKI%UiReB`vSAi zXYG6y2_=0K)O(N}7T;j`#v>a(5jj;W4iW@azRYj99Yqol&C`a&l5W+EZnjO~p5{e+ z&>8YBAx0~{rQt-XMLk+W~**I z$HdVLEL#d8g3MM>&5nQuB|EKVwy2^arWH>AB7s%Sf1Lu^*NK@gpH8y5C> zDYFJlhGDrZ>*o^DM6#s5_WX+eN)wnjnZy%Z!-XxaZaa~ie7E@;GRU@XNX{E*dN`Q~ z$CmpGUI~BK!yaBDerTq@l~?&vBMB4MW&5hv?;%d`BYwj%4t3Ta_^6-)^S&5cWFj6w?D+h!?!UWrDA zAHo98^x0bpEX2W;r0}g6nj!GBP->EEEgGUzV{buGt;6nrr*?LX&o8{Voz;Rh zt}UwS*T72hWvvcU0&UgFHRX`gbP|{& z5F8T9Rf7+*XI4SDiAcVW2=grnTi#HdTzHzTGP0nf{47}@)Q0S=wv_Z>E?G<_H^3+` z$6%k)vA~F}%H?9$X7v=FY)O(Z%<0t_Y@c}S=E`!4S5HN`nlQkaPpM1Hr9LZ`P3$+H zIn)Hh5l;q3I&6mM!n-LE64@46t7SDu`2dAH_q?86k+lUU0i*Q=r?8S+Y2LzFo~do8 zK(t~*hs~dRE)}gXj2p5-szhX(k;J=YQ#hhYHxeMBPKuDYVj>~og6cy(u;$!U6f4#= zc9*K;F1<>kPcpGdNh_3ktIh}}eYdc=*{0{jt?Q~fJ?H6JH5lAds!V>wgpqKhELP9` zFG`!54|^FHAFf)wNrd@Q@tmJwZkc$lpbkj#hN6it^Fu{D_MiiP0BiwW%mTH}0^ru> zD;^3Bk22Xujm!V{>YVVai&*D6$@|;8HD_ z@D?+q`b{15DKWPakZTi&QUa(=UEW?ae~hUps3LBihaNS;Q3UrD2H=s`+J-0OMIlp| zS-Nx=JBm4oBdLg_ldIo5MRZk>txwzm@k3)`nhPUfwba%^LW_v^fD?<&Q?`@7cTj<~ z5otkPJZ4yBGZnt~vlF1mjZ`_BNNVjB$Vc$svL1<5#|Y0?u4NT&bo*+f+})h zcA=9Sn!^NqmmK)!&O;u_=7J=IMJUwrr6g2zQbyqC1ySFitdwF>+p(7Mx`_b^g-Duf z3XKtm&5gLw+7?b^?4u5aKRSeZCaPKx-^iU3p9nwtn{YpsNRjUtkZscwNMw7ff?N8w z{RfAFMZ)iuw!7ta7*rdsmeZ|iX0Ps949-!nvpm*?#e~P@scA`2q|e@(l72Z}d~||B zB3WCez58D2Ura}iG+l@gayl8smWB~OSBY0DF;A9m&CcHW{bs;0?iw*T8!+m*Xuyx> z;q64U%WaBam9ccR>LHaAyRMXU5nRUT#&IF>4<`?1*bD_%_35S`zDX;HQ zXU1Mj2&{#RrH{_)q7DRu%s!##aXMXMir=L9HpUm%cJ|dG8nfQEOE4p=|7Y8XqCtZD zD1f<1wbl|At!;TyY~WAF#`|*-IlGF}#&irsTeuRDIN(gqz+n9S8cOm3K!NShy%enZ z@yeZKZssM%v_@uO!4R`$z@qoV&s}@!3|A&3wkF= z^|x-O22B~_!#0}*^$NgPGVE;|i1 z4c7N+A>i0cVyI}I|AHo(D>P@)AAEEap-IqInZ zMr4jce3FJdj;Mc&#EC_KnCz^WPX>_oD6^DKG!m|{1wr_{xjQ`e5&V1WV!>0P+=9e5 z3Yez;wn>SS9Up=aBnA&f8Ojd6M*#u!6v7NT*jW}ZH*0f63n6DiLNsLgDc%QR;ZlT# z3!p^{^aODa7ii6x(M)Si$?_n8;kXD^I$|Bi*?3wszF*Td(_)K7G zE+d7rPf}W$_@u_-z?g&S8FePF`-ch3=o*!9zcWW!MPHkM?h55pLC{*Bs=Qo6=PZf@ zaYW^cdD$`jAHD2W$^xd3VtF3-vj7W6%#jTKkt!h@g8e-aBV4p9Hy6_chE=11&8SY`e}V{_7TDgro6~9(Vq4lR;opFSe9lVjAFWa zO|`XPNKG8`Qd&8141_>5oz#WDr%nhPQDT|olOAl57HF2EQ27ECQc+3YDPsPTGL;0Y z1Xs~hV7j0saU=#}z-}%jv~e8Lm4X?pfHPzXc*aW+gk&w&!}+{S#43sCWNcF#0!SN9 zMS$|}OuEHDulYMc!}^cVzd%20CD@>#hW-(Tjz&x1suIkoe%99r48u23cecm2;lq*5 z1Yo>^7X(9V4#dW5TQjG=5vN}fTuKGOk)h4oAetiz#&tn@SMw{-7LYUoK-?b`kHKh;aBDx{4A&>t{EpcT% zgcNEEy*nxHgTGp%V&!%*@$Gc6z34(~1gS%>=xBcg{m=X?Q_zAmCIG+Qt?x{sZ7R9`K}|OFl1>iMD8pRSMSMypm!nQ3;G^ zH@o8|vYV85K9{u*%gG9wQd;PZ*+-^R9o4WA_pB7JB)SJ#ol-{QsSFX8tW_1g>x8%Z z^oUFeJ{D9gCwmnU8E$}|$)2^Wlvw|=S8)#mIl7=qz#H#*_qH|dix#HJcu6hBoIJrW}+XIsA+3BHPF zdLWX2W=0aeZJ+M4^|97o%$v=QiE2t>1cRJat8+dbhNe}&EIsnIqcFuNXQmr@y;@sE z!(uXuou6S{|8<)Er|CkXl2;mk4*N8Mv)Dw@(E*D%lSMZ z#E5Zbx5Q+Gu1cRoy%s-%F0%x_=-w}!-{`MY$O?nO&uGa3H6*)^QUMGHRB$RDNT5^% zf>Cq}sjVQd!@xslO^}@aq$vq&=VT6%o5=#TOPshyjXcsz(39n1M9N4Io0|yT7f^9U zPtNo|8mqi1XCN5~B$E;&&hZ$`uPi!JIg^Wr|G@}|z2kCu#*nolbn|+>$3XCiS>*%} zP4QC6>p>_?k)pFZ%}-^0t)VV4GAs>fJl*Q>Ak9+ObJ(*Tw6cwvIDpoG$^2jvm`_X;?bFy8sjb;6CIX2_lRU z03l%$^>axzJr`O^W?d}&P!$H@!`T2D65sF=%ssSP6G48!%J4bC+O2h}a8AY}tCHa7+454T!Pj0wL!$AXyR}400 z=6I&U9%qlP*1{-?otmdmrSf5e0)_x(unPMpQr8)n_gQ1<2
85~$KedV8#^W)~nIn3Tr%2q=c$f)mj+}v0 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%0Dxu{SEu8}0B&lhN;LbZF?wQvj5lO-4pd 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ⅈtl$lDcD$8s9tTgAJF%mbLXm?B=6aL%q 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�aYR9jw@V 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
      DAZg{OazX$FQ#g~zf*O6)S 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
      >R&=sNrCjJ1+c?H$CpK4xtGMyuKL3x#rOf~ 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

      \n{_escape_html(nodeid)} ({duration:.2f}s)\n") + + # Traceback / longrepr + if failure["longrepr"]: + lines.append("\n#### Traceback\n") + lines.append("```python\n") + lines.append(failure["longrepr"]) + if not failure["longrepr"].endswith("\n"): + lines.append("\n") + lines.append("```\n") + + # Captured output sections (stdout, stderr, log, etc.) + for section_name, section_content in failure["sections"]: + if section_content.strip(): + lines.append(f"\n#### {_escape_html(section_name)}\n") + lines.append("```\n") + lines.append(section_content) + if not section_content.endswith("\n"): + lines.append("\n") + lines.append("```\n") + + lines.append("\n
      \n\n") + + with open(summary_path, "a", encoding="utf-8") as f: + f.writelines(lines) + + +def _escape_html(text: str) -> str: + """Escape HTML special characters for safe rendering in markdown.""" + return ( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + ) From 02103846151aa36ae376f13dfd352e08a4271602 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 20:17:39 +0100 Subject: [PATCH 279/291] dtoolbase: Fix static destructor ordering issue --- dtool/src/dtoolbase/deletedBufferChain.cxx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dtool/src/dtoolbase/deletedBufferChain.cxx b/dtool/src/dtoolbase/deletedBufferChain.cxx index f4d269608b..13df8a8b71 100644 --- a/dtool/src/dtoolbase/deletedBufferChain.cxx +++ b/dtool/src/dtoolbase/deletedBufferChain.cxx @@ -167,8 +167,9 @@ get_deleted_chain(size_t buffer_size) { static MutexImpl lock; lock.lock(); - static std::set deleted_chains; - DeletedBufferChain *result = (DeletedBufferChain *)&*deleted_chains.insert(DeletedBufferChain(buffer_size)).first; + alignas(std::set) static char storage[sizeof(std::set)]; + static auto *deleted_chains = new (storage) std::set; + DeletedBufferChain *result = (DeletedBufferChain *)&*deleted_chains->insert(DeletedBufferChain(buffer_size)).first; lock.unlock(); return result; } From c2760c88c6def7642e37b99e2f6a6163bc19be61 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 20:23:13 +0100 Subject: [PATCH 280/291] putil: Fix out-of-bounds read in SparseArray uncovered by ASan --- panda/src/putil/sparseArray.cxx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/panda/src/putil/sparseArray.cxx b/panda/src/putil/sparseArray.cxx index fdad9246a1..e475dbd24c 100644 --- a/panda/src/putil/sparseArray.cxx +++ b/panda/src/putil/sparseArray.cxx @@ -588,6 +588,10 @@ do_intersection(const SparseArray &other) { do_remove_range(other._subranges[i]._end, other._subranges[i + 1]._begin); } + if (_subranges.empty()) { + return; + } + int my_end = (*(_subranges.begin() + _subranges.size() - 1))._end; int other_end = (*(other._subranges.begin() + other._subranges.size() - 1))._end; do_remove_range(other_end, my_end); From ebb556a8668bc100fa34e1fa93e4186e1c8df79e Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 14:07:42 +0100 Subject: [PATCH 281/291] CMake: build unit test runner --- CMakeLists.txt | 14 +++----------- tests/CMakeLists.txt | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 tests/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index fba23f92d6..6a3362badb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,7 @@ option(BUILD_DIRECT "Build the direct source tree." ON) option(BUILD_PANDATOOL "Build the pandatool source tree." ON) option(BUILD_CONTRIB "Build the contrib source tree." ON) option(BUILD_MODELS "Build/install the built-in models." ON) +option(BUILD_TESTS "Build unit test runner." ON) option(BUILD_TOOLS "Build binary tools." ON) # Include Panda3D packages @@ -166,17 +167,8 @@ if(BUILD_MODELS) COMPONENT Models DESTINATION ${CMAKE_INSTALL_DATADIR}/panda3d) endif() -if(INTERROGATE_PYTHON_INTERFACE) - # If we built the Python interface, run the test suite. Note, we do NOT test - # for pytest before adding this test. If the user doesn't have pytest, we'd - # like for the tests to fail. - - # In the Coverage configuration, we also require pytest-cov - - add_test(NAME pytest - COMMAND "${PYTHON_EXECUTABLE}" -m pytest "${PROJECT_SOURCE_DIR}/tests" - $<$:--cov=.> - WORKING_DIRECTORY "${PANDA_OUTPUT_DIR}") +if(BUILD_TESTS) + add_subdirectory(tests "${CMAKE_BINARY_DIR}/tests") endif() # Generate the Panda3DConfig.cmake file so find_package(Panda3D) works, and diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000000..f3c56a38bc --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,20 @@ +if(NOT INTERROGATE_PYTHON_INTERFACE) + return() +endif() + +add_executable(run_pytest main.c) +target_link_libraries(run_pytest panda) +target_link_libraries(run_pytest Python::Python) + +# Note, we do NOT test for pytest before adding this test. If the user doesn't +# have pytest, we'd like for the tests to fail. + +# In the Coverage configuration, we also require pytest-cov + +# To prevent an empty argument from being passed in in non-Coverage builds, +# which will sometimes cause pytest to read all tests in the current directory, +# substitute a nonsensical argument that will be ignored instead +add_test(NAME pytest COMMAND run_pytest tests $,--cov=.,--ignore=nonexistent> + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}") + +set_tests_properties(pytest PROPERTIES ENVIRONMENT "PYTHONPATH=${PANDA_OUTPUT_DIR}") From b963450b45f3f162f134b709a6206aead5e0b818 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 21:47:43 +0100 Subject: [PATCH 282/291] CMake: Add ENABLE_ASAN options to build with address sanitizer --- dtool/CompilerFlags.cmake | 10 ++++++++++ dtool/Config.cmake | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/dtool/CompilerFlags.cmake b/dtool/CompilerFlags.cmake index 31d96279e3..d50e5e2238 100644 --- a/dtool/CompilerFlags.cmake +++ b/dtool/CompilerFlags.cmake @@ -51,6 +51,16 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GCC") endif() +# AddressSanitizer +if(ENABLE_ASAN) + if(CMAKE_CXX_COMPILER_ID MATCHES "(AppleClang|Clang|GNU)") + add_compile_options(-fsanitize=address -fno-omit-frame-pointer) + add_link_options(-fsanitize=address) + else() + message(FATAL_ERROR "ENABLE_ASAN requires GCC or Clang") + endif() +endif() + # Panda3D is now a C++14 project. set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/dtool/Config.cmake b/dtool/Config.cmake index de8568ce8a..cfc2dbfca3 100644 --- a/dtool/Config.cmake +++ b/dtool/Config.cmake @@ -425,6 +425,14 @@ endif() unset(_prefer_mimalloc) +# +# Sanitizers +# + +option(ENABLE_ASAN + "Enable AddressSanitizer for detecting memory errors such as +buffer overflows, use-after-free, etc. Requires GCC or Clang." OFF) + # # This section relates to mobile-device/phone support and options # From b9af71db7fa8b75c459886b3ac868b58a8a2aaf5 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 21:46:58 +0100 Subject: [PATCH 283/291] workflow: Enable address sanitizer for various CMake builds --- .github/workflows/ci.yml | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69558d097f..96abe0df3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: python: YES eigen: NO double: YES + asan: YES - profile: ubuntu-bionic-coverage-ninja os: ubuntu-22.04 @@ -40,6 +41,7 @@ jobs: python: YES eigen: NO double: NO + asan: YES #- profile: macos-coverage-unity-xcode # os: macOS-15 @@ -62,6 +64,7 @@ jobs: python: YES eigen: NO double: NO + asan: YES - profile: windows-standard-unity-msvc os: windows-2022 @@ -182,8 +185,13 @@ jobs: -D HAVE_PYTHON=${{ runner.os != 'Windows' && matrix.python || 'NO' }} -D HAVE_EIGEN=${{ matrix.eigen }} -D STDFLOAT_DOUBLE=${{ matrix.double }} + -D ENABLE_ASAN=${{ matrix.asan || 'NO' }} .. + echo "$PWD/${{ matrix.config }}/bin" >> $GITHUB_PATH + + echo "$PWD/bin" >> $GITHUB_PATH + - name: Build (no Python) if: contains(matrix.python, 'NO') # BEGIN A @@ -216,11 +224,12 @@ jobs: shell: bash env: PYTHONPATH: ${{ matrix.config }} + ASAN_OPTIONS: detect_leaks=0:detect_odr_violation=0:log_path=asan.log.py38 run: | PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//') $PYTHON_EXECUTABLE -m pip install -r ../requirements-test.txt export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw - $PYTHON_EXECUTABLE -m pytest ../tests --cov=. + run_pytest ../tests --cov=. # END B - name: Setup Python (Python 3.9) @@ -248,11 +257,12 @@ jobs: shell: bash env: PYTHONPATH: ${{ matrix.config }} + ASAN_OPTIONS: detect_leaks=0:detect_odr_violation=0:log_path=asan.log.py39 run: | PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//') $PYTHON_EXECUTABLE -m pip install -r ../requirements-test.txt export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw - $PYTHON_EXECUTABLE -m pytest ../tests --cov=. + run_pytest ../tests --cov=. # END B - name: Setup Python (Python 3.10) @@ -280,11 +290,12 @@ jobs: shell: bash env: PYTHONPATH: ${{ matrix.config }} + ASAN_OPTIONS: detect_leaks=0:detect_odr_violation=0:log_path=asan.log.py310 run: | PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//') $PYTHON_EXECUTABLE -m pip install -r ../requirements-test.txt export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw - $PYTHON_EXECUTABLE -m pytest ../tests --cov=. + run_pytest ../tests --cov=. # END B - name: Setup Python (Python 3.11) @@ -312,11 +323,12 @@ jobs: shell: bash env: PYTHONPATH: ${{ matrix.config }} + ASAN_OPTIONS: detect_leaks=0:detect_odr_violation=0:log_path=asan.log.py311 run: | PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//') $PYTHON_EXECUTABLE -m pip install -r ../requirements-test.txt export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw - $PYTHON_EXECUTABLE -m pytest ../tests --cov=. + run_pytest ../tests --cov=. # END B - name: Setup Python (Python 3.12) @@ -344,13 +356,26 @@ jobs: shell: bash env: PYTHONPATH: ${{ matrix.config }} + ASAN_OPTIONS: detect_leaks=0:detect_odr_violation=0:log_path=asan.log.py312 run: | PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//') $PYTHON_EXECUTABLE -m pip install -r ../requirements-test.txt export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw - $PYTHON_EXECUTABLE -m pytest ../tests --cov=. + run_pytest ../tests --cov=. # END B + - name: Show ASan errors + if: failure() && contains(matrix.asan, 'YES') + working-directory: build + run: | + ls -lAh asan.log.* || true + if cat asan.log.* 2>/dev/null | grep -q .; then + echo '### Address Sanitizer Report' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat asan.log.* >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "No ASan errors found" + echo '```' >> $GITHUB_STEP_SUMMARY + fi + - name: Upload coverage reports if: always() && matrix.config == 'Coverage' working-directory: build From 4f5a374c858a544e674a33c5c5e0ba09984d3b21 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 21:44:51 +0100 Subject: [PATCH 284/291] workflow: Show coverage result in GitHub step summary --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96abe0df3b..2902ae3ad0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -389,6 +389,13 @@ jobs: python -m pip install coverage python -m coverage combine $(find . -name '.coverage.*') + TOTAL=$(python -m coverage report --format=total) + echo '### Coverage Report' >> $GITHUB_STEP_SUMMARY + echo "
      Coverage: **${TOTAL}%**" >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + python -m coverage report --format=markdown >> $GITHUB_STEP_SUMMARY + echo '
      ' >> $GITHUB_STEP_SUMMARY + llvm-profdata merge pid-*.profraw -o coverage.profdata llvm-cov show $(grep -Rl LLVM_PROFILE_FILE . | sed 's/^/-object /') -instr-profile=coverage.profdata > coverage.txt bash <(curl -s https://codecov.io/bash) -y ../.github/codecov.yml From 051268de51721d2d2fcb12a984547396a064aecf Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 22:54:38 +0100 Subject: [PATCH 285/291] workflow: Fix very minor formatting issue in coverage summary [skip ci] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2902ae3ad0..962e946b8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -391,7 +391,7 @@ jobs: TOTAL=$(python -m coverage report --format=total) echo '### Coverage Report' >> $GITHUB_STEP_SUMMARY - echo "
      Coverage: **${TOTAL}%**" >> $GITHUB_STEP_SUMMARY + echo "
      Coverage: ${TOTAL}%" >> $GITHUB_STEP_SUMMARY echo '' >> $GITHUB_STEP_SUMMARY python -m coverage report --format=markdown >> $GITHUB_STEP_SUMMARY echo '
      ' >> $GITHUB_STEP_SUMMARY From 1c8a5cfb48b829029a7c5b7f453cf28cbb2754d4 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 26 Jan 2026 11:08:56 +0100 Subject: [PATCH 286/291] audio: AudioLoadRequest should take Filename or MovieAudio --- panda/src/audio/audioLoadRequest.I | 17 +++++++++++++++-- panda/src/audio/audioLoadRequest.cxx | 4 +++- panda/src/audio/audioLoadRequest.h | 10 +++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/panda/src/audio/audioLoadRequest.I b/panda/src/audio/audioLoadRequest.I index f5d9b8f5f8..baeb3847af 100644 --- a/panda/src/audio/audioLoadRequest.I +++ b/panda/src/audio/audioLoadRequest.I @@ -16,7 +16,7 @@ * to begin an asynchronous load. */ INLINE AudioLoadRequest:: -AudioLoadRequest(AudioManager *audio_manager, const std::string &filename, +AudioLoadRequest(AudioManager *audio_manager, const Filename &filename, bool positional) : _audio_manager(audio_manager), _filename(filename), @@ -24,6 +24,19 @@ AudioLoadRequest(AudioManager *audio_manager, const std::string &filename, { } +/** + * Create a new AudioLoadRequest, and add it to the loader via load_async(), + * to begin an asynchronous load. + */ +INLINE AudioLoadRequest:: +AudioLoadRequest(AudioManager *audio_manager, MovieAudio *source, + bool positional) : + _audio_manager(audio_manager), + _source(source), + _positional(positional) +{ +} + /** * Returns the AudioManager that will serve this asynchronous * AudioLoadRequest. @@ -36,7 +49,7 @@ get_audio_manager() const { /** * Returns the filename associated with this asynchronous AudioLoadRequest. */ -INLINE const std::string &AudioLoadRequest:: +INLINE const Filename &AudioLoadRequest:: get_filename() const { return _filename; } diff --git a/panda/src/audio/audioLoadRequest.cxx b/panda/src/audio/audioLoadRequest.cxx index e3a04d38b8..138979de83 100644 --- a/panda/src/audio/audioLoadRequest.cxx +++ b/panda/src/audio/audioLoadRequest.cxx @@ -21,7 +21,9 @@ TypeHandle AudioLoadRequest::_type_handle; */ AsyncTask::DoneStatus AudioLoadRequest:: do_task() { - set_result(_audio_manager->get_sound(_filename, _positional)); + set_result(_source != nullptr + ? _audio_manager->get_sound(_source, _positional) + : _audio_manager->get_sound(_filename, _positional)); // Don't continue the task; we're done. return DS_done; diff --git a/panda/src/audio/audioLoadRequest.h b/panda/src/audio/audioLoadRequest.h index 1c07098b91..5251a67188 100644 --- a/panda/src/audio/audioLoadRequest.h +++ b/panda/src/audio/audioLoadRequest.h @@ -33,11 +33,14 @@ public: PUBLISHED: INLINE explicit AudioLoadRequest(AudioManager *audio_manager, - const std::string &filename, + const Filename &filename, + bool positional); + INLINE explicit AudioLoadRequest(AudioManager *audio_manager, + MovieAudio *source, bool positional); INLINE AudioManager *get_audio_manager() const; - INLINE const std::string &get_filename() const; + INLINE const Filename &get_filename() const; INLINE bool get_positional() const; INLINE bool is_ready() const; @@ -48,7 +51,8 @@ protected: private: PT(AudioManager) _audio_manager; - std::string _filename; + Filename _filename; + PT(MovieAudio) _source; bool _positional; public: From 8cd3812ecbf67039401027cea03740beeb1427b4 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 26 Jan 2026 11:09:31 +0100 Subject: [PATCH 287/291] glgsg: Track uploaded byte count in PStats for SSBO updates --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 40ed2a61e8..d5b98d8418 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -7829,6 +7829,9 @@ update_shader_buffer_data(ShaderBuffer *buffer, size_t start, size_t size, } report_my_gl_errors(); + if (data != nullptr || !_supports_clear_buffer) { + _data_transferred_pcollector.add_level(size); + } return false; } From d84b984e22ddffacecb9052be143749c0fa366e8 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 26 Jan 2026 11:10:27 +0100 Subject: [PATCH 288/291] showbase: Fix exception when calling Ctrl+C at the wrong moment --- direct/src/showbase/ShowBase.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index e5c2e79376..78ecaaf763 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -711,7 +711,8 @@ class ShowBase(DirectObject.DirectObject): allowAccessibilityShortcutKeys(True) self.__disabledStickyKeys = False - self.__directObject.ignoreAll() + if hasattr(self, '__directObject'): + self.__directObject.ignoreAll() self.ignoreAll() self.shutdown() From 99bbc14973df47c4cecdad0f4f10f5582c298d49 Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 26 Jan 2026 11:11:00 +0100 Subject: [PATCH 289/291] loader: Fix incomplete loadModel/loadSound type annotations --- direct/src/showbase/Loader.py | 39 ++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index 6e49ebf53c..bb106577c2 100644 --- a/direct/src/showbase/Loader.py +++ b/direct/src/showbase/Loader.py @@ -17,6 +17,7 @@ from panda3d.core import ( ModelFlattenRequest, ModelNode, ModelPool, + MovieAudio, NodePath, PandaNode, SamplerState, @@ -31,12 +32,17 @@ from direct.showbase.DirectObject import DirectObject from . import ShowBase import warnings import sys -from typing import Any, Callable, Iterable +import os +from typing import Any, Callable, Iterable, Union + +# Type aliases for loadModel, loadSound, etc. annotations +_ModelPath = Union[str, Filename, os.PathLike[str]] +_SoundPath = Union[str, Filename, os.PathLike[str], MovieAudio] # You can specify a phaseChecker callback to check # a modelPath to see if it is being loaded in the correct # phase -phaseChecker: Callable[[str, LoaderOptions], object] | None = None +phaseChecker: Callable[[_ModelPath, LoaderOptions], object] | None = None class Loader(DirectObject): @@ -183,7 +189,7 @@ class Loader(DirectObject): # model loading funcs def loadModel( self, - modelPath: str | list[str] | tuple[str, ...] | set[str], + modelPath: _ModelPath | list[_ModelPath] | tuple[_ModelPath, ...] | set[_ModelPath], loaderOptions: LoaderOptions | None = None, noCache: bool | None = None, allowInstance: bool = False, @@ -276,7 +282,7 @@ class Loader(DirectObject): if allowInstance: loaderOptions.setFlags(loaderOptions.getFlags() | LoaderOptions.LFAllowInstance) - modelList: Iterable[str] + modelList: Iterable[_ModelPath] if not isinstance(modelPath, (tuple, list, set)): # We were given a single model pathname. modelList = [modelPath] @@ -971,7 +977,13 @@ class Loader(DirectObject): TexturePool.releaseTexture(texture) # sound loading funcs - def loadSfx(self, *args, **kw) -> Any: + def loadSfx( + self, + soundPath: _SoundPath | tuple[_SoundPath, ...] | list[_SoundPath] | set[_SoundPath], + positional: bool = False, + callback: Callable[..., object] | None = None, + extraArgs: list = [], + ) -> Any: """Loads one or more sound files, specifically designated as a "sound effect" file (that is, uses the sfxManager to load the sound). There is no distinction between sound effect files @@ -983,10 +995,16 @@ class Loader(DirectObject): # showbase-created sfxManager should always be at front of list assert self.base is not None if self.base.sfxManagerList: - return self.loadSound(self.base.sfxManagerList[0], *args, **kw) + return self.loadSound(self.base.sfxManagerList[0], soundPath, positional, callback, extraArgs) return None - def loadMusic(self, *args, **kw): + def loadMusic( + self, + soundPath: _SoundPath | tuple[_SoundPath, ...] | list[_SoundPath] | set[_SoundPath], + positional: bool = False, + callback: Callable[..., object] | None = None, + extraArgs: list = [], + ) -> Any: """Loads one or more sound files, specifically designated as a "music" file (that is, uses the musicManager to load the sound). There is no distinction between sound effect files @@ -994,15 +1012,16 @@ class Loader(DirectObject): to load the sound file, but this distinction allows the sound effects and/or the music files to be adjusted as a group, independently of the other group.""" + assert self.base is not None if self.base.musicManager: - return self.loadSound(self.base.musicManager, *args, **kw) + return self.loadSound(self.base.musicManager, soundPath, positional, callback, extraArgs) else: return None def loadSound( self, manager: AudioManager, - soundPath: str | tuple[str, ...] | list[str] | set[str], + soundPath: _SoundPath | tuple[_SoundPath, ...] | list[_SoundPath] | set[_SoundPath], positional: bool = False, callback: Callable[..., object] | None = None, extraArgs: list = [], @@ -1016,7 +1035,7 @@ class Loader(DirectObject): from panda3d.core import AudioLoadRequest - soundList: Iterable[str] + soundList: Iterable[_SoundPath] if not isinstance(soundPath, (tuple, list, set)): # We were given a single sound pathname or a MovieAudio instance. soundList = [soundPath] From 8bbc1a15153250cfc94ad3fc4af48b336faa5dad Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 26 Jan 2026 14:29:04 +0100 Subject: [PATCH 290/291] loader: Restore compatibility with Python 3.8 --- direct/src/showbase/Loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/direct/src/showbase/Loader.py b/direct/src/showbase/Loader.py index bb106577c2..9381c8f4e7 100644 --- a/direct/src/showbase/Loader.py +++ b/direct/src/showbase/Loader.py @@ -36,8 +36,8 @@ import os from typing import Any, Callable, Iterable, Union # Type aliases for loadModel, loadSound, etc. annotations -_ModelPath = Union[str, Filename, os.PathLike[str]] -_SoundPath = Union[str, Filename, os.PathLike[str], MovieAudio] +_ModelPath = Union[str, Filename, os.PathLike] +_SoundPath = Union[str, Filename, os.PathLike, MovieAudio] # You can specify a phaseChecker callback to check # a modelPath to see if it is being loaded in the correct From f2c680ca297ccef816bce8f95fd59c694818028c Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 27 Jan 2026 16:09:51 +0100 Subject: [PATCH 291/291] linmath: Fix wrong type printed in LMatrix3 repr [skip ci] --- panda/src/linmath/lmatrix3_ext_src.I | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/linmath/lmatrix3_ext_src.I b/panda/src/linmath/lmatrix3_ext_src.I index f5759e3821..638f010d67 100644 --- a/panda/src/linmath/lmatrix3_ext_src.I +++ b/panda/src/linmath/lmatrix3_ext_src.I @@ -47,7 +47,7 @@ __rmul__(FLOATTYPE scalar) const { */ INLINE_LINMATH std::string Extension:: __repr__() const { - char buf[32 * 17] = "LMatrix4"; + char buf[32 * 17] = "LMatrix3"; char *p = buf + strlen(buf); *(p++) = FLOATTOKEN; *(p++) = '(';

      `PH}&eOnvlw95@T_TJP!HGAuUNhML`d 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 Date: Tue, 20 Jan 2026 11:42:05 +0100 Subject: [PATCH 271/291] makepanda: Add /usr/local/lib64 to searched library paths [skip ci] --- makepanda/makepandacore.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/makepanda/makepandacore.py b/makepanda/makepandacore.py index cfc19fb1ad..a34603691f 100644 --- a/makepanda/makepandacore.py +++ b/makepanda/makepandacore.py @@ -3031,6 +3031,13 @@ def SetupBuildEnvironment(compiler): libdir += '64' SYS_LIB_DIRS += [libdir] + if sys.platform != "darwin": + # Some Linux distributions (eg. Fedora) don't add /usr/local/lib64 + if ("/usr/lib64" in SYS_LIB_DIRS and + "/usr/local/lib64" not in SYS_LIB_DIRS and + os.path.isdir("/usr/local/lib64")): + SYS_LIB_DIRS.append("/usr/local/lib64") + # Now extract the preprocessor's include directories. cmd = GetCXX() + " -x c++ -v -E " + os.devnull cmd += sysroot_flag From 9afb3140c8537cb26946dc0720afbf8fdee8f755 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 20 Jan 2026 18:54:51 +0100 Subject: [PATCH 272/291] glgsg: Don't assume buffer clear is necessarily supported --- .../glstuff/glGraphicsStateGuardian_src.cxx | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 977cbec9e0..ba8741bd0f 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -7794,24 +7794,38 @@ update_shader_buffer_data(ShaderBuffer *buffer, size_t start, size_t size, _glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); } - if (data != nullptr) { - if (_supports_dsa) { +#ifndef OPENGLES + if (_supports_dsa) { + if (data != nullptr) { _glNamedBufferSubData(gbc->_index, start, size, data); - } else { - _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); - _glBufferSubData(GL_SHADER_STORAGE_BUFFER, start, size, data); - _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - _current_sbuffer_index = 0; } - } else { - if (_supports_dsa) { + else if (_supports_clear_buffer) { _glClearNamedBufferSubData(gbc->_index, GL_R8, start, size, GL_RED, GL_UNSIGNED_BYTE, nullptr); - } else { - _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); - _glClearBufferSubData(GL_SHADER_STORAGE_BUFFER, GL_R8, start, size, GL_RED, GL_UNSIGNED_BYTE, nullptr); - _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - _current_sbuffer_index = 0; } + else { + void *null_data = calloc(1, size); + _glNamedBufferSubData(gbc->_index, start, size, null_data); + free(null_data); + } + } else +#endif + { + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbc->_index); + if (data != nullptr) { + _glBufferSubData(GL_SHADER_STORAGE_BUFFER, start, size, data); + } +#ifndef OPENGLES + else if (_supports_clear_buffer) { + _glClearBufferSubData(GL_SHADER_STORAGE_BUFFER, GL_R8, start, size, GL_RED, GL_UNSIGNED_BYTE, nullptr); + } +#endif + else { + void *null_data = calloc(1, size); + _glBufferSubData(GL_SHADER_STORAGE_BUFFER, start, size, null_data); + free(null_data); + } + _glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + _current_sbuffer_index = 0; } report_my_gl_errors(); From 004a3c85727363add7fb65eb89fe389ba7648490 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 20 Jan 2026 18:55:14 +0100 Subject: [PATCH 273/291] Update mentions of LambdaTest in sponsors list [skip ci] --- BACKERS.md | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BACKERS.md b/BACKERS.md index 29838e55e9..269e617d67 100644 --- a/BACKERS.md +++ b/BACKERS.md @@ -5,12 +5,12 @@ This is a list of all the people who are contributing financially to Panda3D. I ## Bronze Sponsors [Route4Me](https://route4me.com/) -[LambdaTest](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor) +[TestMu AI](https://www.testmu.ai/?utm_source=panda3d&utm_medium=sponsor) * [Daniel Stokes](https://opencollective.com/daniel-stokes) * [David Rose](https://opencollective.com/david-rose) * [Route4Me](https://route4me.com/) -* [LambdaTest](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor) +* [TestMu AI](https://www.testmu.ai/?utm_source=panda3d&utm_medium=sponsor) ## Benefactors diff --git a/README.md b/README.md index 8211bbb247..cbfd7fc84d 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ visit the [Sponsors page](https://www.panda3d.org/sponsors) on our web site. Thank you to everyone who has donated! [Route4Me](https://route4me.com/) -[LambdaTest](https://www.lambdatest.com/?utm_source=panda3d&utm_medium=sponsor) +[TestMu AI](https://www.testmu.ai/?utm_source=panda3d&utm_medium=sponsor) From 9edbdbf6c9beaa074dafac3a71876a9819c601dc Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 10:17:10 +0100 Subject: [PATCH 274/291] glgsg: Fix mipmap level depth when uploading 3D textures --- panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index ba8741bd0f..40ed2a61e8 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -14919,6 +14919,7 @@ upload_texture_view(CLP(TextureContext) *gtc, int view, bool needs_reload, int width = tex->get_expected_mipmap_x_size(n); int height = tex->get_expected_mipmap_y_size(n); + int depth = tex->get_expected_mipmap_z_size(n); // we grab the mipmap pointer first, if it is NULL we grab the normal // mipmap image pointer which is a PTA_uchar From 626e419972ca21a41dd4a7d26b3d1c3d4368a246 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 13:23:00 +0100 Subject: [PATCH 275/291] x11: Fix various very minor memory leaks --- panda/src/glxdisplay/glxGraphicsStateGuardian.cxx | 5 +++++ panda/src/x11display/x11GraphicsWindow.cxx | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/panda/src/glxdisplay/glxGraphicsStateGuardian.cxx b/panda/src/glxdisplay/glxGraphicsStateGuardian.cxx index 83ea5f2b2f..be8c5f7f10 100644 --- a/panda/src/glxdisplay/glxGraphicsStateGuardian.cxx +++ b/panda/src/glxdisplay/glxGraphicsStateGuardian.cxx @@ -402,6 +402,7 @@ choose_pixel_format(const FrameBufferProperties &properties, << ", context_has_pixmap = " << _context_has_pixmap << "\n"; } + XFree(configs); return; } } @@ -414,6 +415,10 @@ choose_pixel_format(const FrameBufferProperties &properties, _visuals = nullptr; } + if (configs != nullptr) { + XFree(configs); + } + glxdisplay_cat.warning() << "No suitable FBConfig contexts available; using XVisual only.\n" << _fbprops << "\n"; diff --git a/panda/src/x11display/x11GraphicsWindow.cxx b/panda/src/x11display/x11GraphicsWindow.cxx index 90d1ee0d44..b3363ff110 100644 --- a/panda/src/x11display/x11GraphicsWindow.cxx +++ b/panda/src/x11display/x11GraphicsWindow.cxx @@ -634,10 +634,10 @@ process_events() { Atom type; int format; unsigned long nItem, bytesAfter; - unsigned char *new_window_properties = NULL; + unsigned char *new_window_properties = nullptr; // gather all properties from the active dispplay and window XGetWindowProperty(_display, _xwindow, wmState, 0, LONG_MAX, false, AnyPropertyType, &type, &format, &nItem, &bytesAfter, &new_window_properties); - if (nItem > 0) { + if (nItem > 0 && new_window_properties != nullptr) { x11GraphicsPipe *x11_pipe; DCAST_INTO_V(x11_pipe, _pipe); // run through all found items @@ -650,6 +650,7 @@ process_events() { is_maximized = true; } } + XFree(new_window_properties); } // Debug entry @@ -1532,6 +1533,9 @@ set_wm_properties(const WindowProperties &properties, bool already_mapped) { XSetWMProperties(_display, _xwindow, window_name_p, window_name_p, nullptr, 0, size_hints_p, wm_hints_p, class_hints_p); + if (window_name_p != nullptr) { + XFree(window_name.value); + } if (size_hints_p != nullptr) { XFree(size_hints_p); } From 49955343be4a02f186e7673ea0207104fa6834a1 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 13:23:28 +0100 Subject: [PATCH 276/291] grutil: Add muted/volume properties to HTMLVideoTexture --- panda/src/grutil/htmlVideoTexture.cxx | 41 +++++++++++++++++++++++++++ panda/src/grutil/htmlVideoTexture.h | 13 +++++++++ 2 files changed, 54 insertions(+) diff --git a/panda/src/grutil/htmlVideoTexture.cxx b/panda/src/grutil/htmlVideoTexture.cxx index cc083ea225..c5182f141d 100644 --- a/panda/src/grutil/htmlVideoTexture.cxx +++ b/panda/src/grutil/htmlVideoTexture.cxx @@ -229,6 +229,47 @@ is_playing() const { }, this); } +/** + * Returns true if the video own audio is muted. True by default. + */ +bool HTMLVideoTexture:: +is_muted() const { + return EM_ASM_INT({ + return window._htmlVideoData[$0].video.muted; + }, this); +} + +/** + * Sets whether the video's own audio is muted. + */ +void HTMLVideoTexture:: +set_muted(bool muted) { + EM_ASM({ + window._htmlVideoData[$0].video.muted = ($1 !== 0); + }, this, muted); +} + +/** + * Returns the audio volume of the video element, in the range 0.0 to 1.0. + */ +double HTMLVideoTexture:: +get_volume() const { + return EM_ASM_DOUBLE({ + return window._htmlVideoData[$0].video.volume; + }, this); +} + +/** + * Sets the audio volume in the range 0.0 to 1.0. Note that you must also + * unmute the audio using the muted property. + */ +void HTMLVideoTexture:: +set_volume(double volume) { + EM_ASM({ + window._htmlVideoData[$0].video.volume = $1; + }, this, volume); +} + /** * A factory function to make a new HTMLVideoTexture, used to pass to the * TexturePool. diff --git a/panda/src/grutil/htmlVideoTexture.h b/panda/src/grutil/htmlVideoTexture.h index 2e0c03f561..e8a1f6a023 100644 --- a/panda/src/grutil/htmlVideoTexture.h +++ b/panda/src/grutil/htmlVideoTexture.h @@ -25,6 +25,10 @@ * - No RAM access of video data is possible * - No explicit synchronization with audio * - No loop count + * + * Unlike MovieTexture, this has an additional "muted" attribute which is true + * by default but can be set to false in order to play the video's audio via + * the browser interface. The "volume" property can configure the volume. */ class EXPCL_PANDA_GRUTIL HTMLVideoTexture : public Texture { PUBLISHED: @@ -47,6 +51,12 @@ PUBLISHED: double get_play_rate() const; bool is_playing() const; +public: + bool is_muted() const; + void set_muted(bool muted); + double get_volume() const; + void set_volume(double volume); + PUBLISHED: MAKE_PROPERTY(video_length, get_video_length); MAKE_PROPERTY(video_width, get_video_width); @@ -57,6 +67,9 @@ PUBLISHED: MAKE_PROPERTY(play_rate, get_play_rate, set_play_rate); MAKE_PROPERTY(playing, is_playing); + MAKE_PROPERTY(muted, is_muted, set_muted); + MAKE_PROPERTY(volume, get_volume, set_volume); + public: static PT(Texture) make_texture(); From 4b989107893ca3dfcceeb8877a69da0b1e998f62 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 11:25:47 +0100 Subject: [PATCH 277/291] Use static storage instead of heap for forever-objects (squelches ASan) --- panda/src/pgraph/renderAttrib.cxx | 3 ++- panda/src/pgraph/renderState.cxx | 6 ++++-- panda/src/pgraph/transformState.cxx | 3 ++- panda/src/pstatclient/pStatClient.cxx | 14 ++++++++++++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/panda/src/pgraph/renderAttrib.cxx b/panda/src/pgraph/renderAttrib.cxx index 0992dcb79c..4f48d5f9fd 100644 --- a/panda/src/pgraph/renderAttrib.cxx +++ b/panda/src/pgraph/renderAttrib.cxx @@ -478,7 +478,8 @@ init_attribs() { // _attribs_lock without a startup race condition. For the meantime, this // is OK because we guarantee that this method is called at static init // time, presumably when there is still only one thread in the world. - _attribs_lock = new LightReMutex("RenderAttrib::_attribs_lock"); + alignas(LightReMutex) static char storage[sizeof(LightReMutex)]; + _attribs_lock = new (storage) LightReMutex("RenderAttrib::_attribs_lock"); nassertv(Thread::get_current_thread() == Thread::get_main_thread()); } diff --git a/panda/src/pgraph/renderState.cxx b/panda/src/pgraph/renderState.cxx index 235075cb1d..423ca6434f 100644 --- a/panda/src/pgraph/renderState.cxx +++ b/panda/src/pgraph/renderState.cxx @@ -1841,13 +1841,15 @@ init_states() { // _states_lock without a startup race condition. For the meantime, this is // OK because we guarantee that this method is called at static init time, // presumably when there is still only one thread in the world. - _states_lock = new LightReMutex("RenderState::_states_lock"); + alignas(LightReMutex) static char storage[sizeof(LightReMutex)]; + _states_lock = new (storage) LightReMutex("RenderState::_states_lock"); _cache_stats.init(); nassertv(Thread::get_current_thread() == Thread::get_main_thread()); // Initialize the empty state object as well. It is used so often that it // is declared globally, and lives forever. - RenderState *state = new RenderState; + alignas(RenderState) static char state_storage[sizeof(RenderState)]; + RenderState *state = new (state_storage) RenderState; state->local_object(); state->cache_ref_only(); state->_saved_entry = _states.store(state, nullptr); diff --git a/panda/src/pgraph/transformState.cxx b/panda/src/pgraph/transformState.cxx index f02e77203a..c8e27962da 100644 --- a/panda/src/pgraph/transformState.cxx +++ b/panda/src/pgraph/transformState.cxx @@ -1387,7 +1387,8 @@ init_states() { // _states_lock without a startup race condition. For the meantime, this is // OK because we guarantee that this method is called at static init time, // presumably when there is still only one thread in the world. - _states_lock = new LightReMutex("TransformState::_states_lock"); + alignas(LightReMutex) static char storage[sizeof(LightReMutex)]; + _states_lock = new (storage) LightReMutex("TransformState::_states_lock"); _cache_stats.init(); nassertv(Thread::get_current_thread() == Thread::get_main_thread()); diff --git a/panda/src/pstatclient/pStatClient.cxx b/panda/src/pstatclient/pStatClient.cxx index 3daa78d3d5..809dd0828a 100644 --- a/panda/src/pstatclient/pStatClient.cxx +++ b/panda/src/pstatclient/pStatClient.cxx @@ -1116,9 +1116,19 @@ add_collector(PStatClient::Collector *collector) { // We need to grow the array. We have to be careful here, because there // might be clients accessing the array right now who are not protected by // the lock. - size_t new_collectors_size = (_collectors_size == 0) ? 128 : _collectors_size * 2; CollectorPointer *old_collectors = _collectors.load(std::memory_order_relaxed); - CollectorPointer *new_collectors = new CollectorPointer[new_collectors_size]; + CollectorPointer *new_collectors; + + static const size_t initial_collectors_size = 256; + static CollectorPointer initial_collectors[initial_collectors_size]; + size_t new_collectors_size; + if (_collectors_size == 0) { + new_collectors_size = initial_collectors_size; + new_collectors = initial_collectors; + } else { + new_collectors_size = _collectors_size * 2; + new_collectors = new CollectorPointer[new_collectors_size]; + } if (old_collectors != nullptr) { memcpy(new_collectors, old_collectors, num_collectors * sizeof(CollectorPointer)); } From 48215debb48ed97d2a0da92a22ca1ac245282e46 Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 24 Jan 2026 14:03:29 +0100 Subject: [PATCH 278/291] tests: When running in GitHub Actions, write GFM tests summary --- tests/conftest.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 4735ef99bf..8b703fe2ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import os import sys import pytest from panda3d import core @@ -99,3 +100,125 @@ def gsg(graphics_pipe, graphics_engine): if buffer is not None: graphics_engine.remove_window(buffer) + + +def pytest_configure(config): + """Initialize the failure collector.""" + config._github_summary_failures = [] + config._github_summary_counts = { + "passed": 0, + "failed": 0, + "skipped": 0, + "xfailed": 0, + } + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + """Collect information about test outcomes.""" + outcome = yield + report = outcome.get_result() + + if report.when == "call": + counts = item.config._github_summary_counts + + if report.passed: + if hasattr(report, "wasxfail"): + # xpass - passed but was expected to fail (treat as passed for now) + counts["passed"] += 1 + else: + counts["passed"] += 1 + elif report.failed: + counts["failed"] += 1 + failure_info = { + "nodeid": report.nodeid, + "location": report.location, + "longrepr": str(report.longrepr) if report.longrepr else None, + "sections": report.sections, + "duration": report.duration, + } + item.config._github_summary_failures.append(failure_info) + elif report.skipped: + if hasattr(report, "wasxfail"): + counts["xfailed"] += 1 + else: + counts["skipped"] += 1 + + elif report.when == "setup" and report.skipped: + # Handle skip during setup (e.g., skipif, skip markers) + item.config._github_summary_counts["skipped"] += 1 + + +def pytest_sessionfinish(session, exitstatus): + """Write GitHub step summary if GITHUB_STEP_SUMMARY is set.""" + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + return + + failures = session.config._github_summary_failures + counts = session.config._github_summary_counts + + lines = [] + + # Build status parts for the summary line + status_parts = [] + if counts["failed"]: + status_parts.append(f"**{counts['failed']}** failed") + if counts["passed"]: + status_parts.append(f"**{counts['passed']}** passed") + if counts["skipped"]: + status_parts.append(f"**{counts['skipped']}** skipped") + if counts["xfailed"]: + status_parts.append(f"**{counts['xfailed']}** xfailed") + + total = sum(counts.values()) + status_line = ", ".join(status_parts) + f" (**{total}** total)\n" + + # Header with overall status + if counts["failed"] == 0: + lines.append(f"### :white_check_mark: All tests passed (Python {sys.version_info.major}.{sys.version_info.minor})\n\n") + else: + lines.append(f"### :x: Test failures (Python {sys.version_info.major}.{sys.version_info.minor})\n\n") + + lines.append(status_line) + lines.append("\n") + + # Each failure in a collapsible section + for failure in failures: + nodeid = failure["nodeid"] + duration = failure["duration"] + + lines.append(f"

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