assimp: Preload AsssimpModels.

This prevents long loading times by preloading AssimpModel objects
from disk when required.

As part of this, the idea of a callback is dropped, and renders of
textures happen when the displayable is rendered. This creates all
Renders in the main thread, which makes Render mark-sweep reliable.
This commit is contained in:
Tom Rothamel
2025-02-09 14:05:04 -05:00
parent d81f8264ff
commit ef40de899c
5 changed files with 176 additions and 118 deletions
+2
View File
@@ -241,6 +241,7 @@ name_blacklist = {
"renpy.savelocation.disk_lock",
"renpy.character.TAG_RE",
"renpy.display.im.cache",
"renpy.display.render.main_thread",
"renpy.display.render.blit_lock",
"renpy.display.render.IDENTITY",
"renpy.loader.auto_lock",
@@ -252,6 +253,7 @@ name_blacklist = {
"renpy.exports.sdl_dll",
"renpy.sl2.slast.serial",
"renpy.gl2.assimp.loader",
"renpy.gl2.assimp.loader_lock",
"renpy.gl2.gl2draw.default_position",
}
+3
View File
@@ -515,6 +515,8 @@ class Cache(object):
def preload_thread_pass(self):
renpy.gl2.assimp.preload()
while self.preloads and self.keep_preloading:
# If the size of the current generation is bigger than the
@@ -544,6 +546,7 @@ class Cache(object):
self.cleanout()
def add_load_log(self, filename):
if not renpy.config.developer:
+2
View File
@@ -209,4 +209,6 @@ def prediction_coroutine(root_widget):
predicting = False
renpy.gl2.assimp.finish_predict()
yield None
-1
View File
@@ -509,7 +509,6 @@ def mark_sweep():
worklist.append(screen_render)
worklist.extend(renpy.display.im.cache.get_renders())
worklist.extend(renpy.gl2.assimp.get_renders())
for r in worklist:
r.mark = True
+169 -117
View File
@@ -19,6 +19,11 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import threading
from typing import Iterable
from pygame_sdl2 cimport *
import_pygame_sdl2()
@@ -105,22 +110,23 @@ class ModelData:
embedded_textures: dict[str, Data]
"The embedded textures in the model."
mesh_renders : list[Render]
"The renders that make up the model."
mesh_info: dict[str, MeshInfo]
"Information required to blit each mesh in the model."
def __init__(self):
self.embedded_textures = { }
self.mesh_renders = [ ]
self.mesh_info = [ ]
cache : dict[str, ModelData] = { }
cache: dict[AssimpModel, ModelData] = { }
"Caches the models that have been loaded."
predicted: set[AssimpModel]|None = None
"The set of models that are predicted to be loaded."
new_predicted: set[AssimpModel] = set()
"The same, but before finish_predict() is called."
def get_renders():
for i in cache.values():
for j in i.mesh_renders:
yield j
def free_memory():
cache.clear()
@@ -128,103 +134,57 @@ def free_memory():
class MeshInfo:
"""
This stores information that's passed into the mesh callback.
This stores the information used to blit a texture.
"""
_importer: "AssetImporter"
"The importer that's loading the mesh."
_material_index: int
"The index of the material."
mesh: Mesh3
"The mesh that's being loaded."
reverse_matrix: Matrix
forward_matrix: Matrix
def has_texture(self, texture_type: str) -> bool:
"""
Returns True if the mesh has a texture of the given type.
"""
if texture_type not in TEXTURE_TYPES:
raise Exception(f"Unknown texture type {texture_type}.")
return self._importer.get_texture(self._material_index, TEXTURE_TYPES[texture_type]) is not None
def get_texture(self, texture_type: str) -> renpy.display.displayable.Displayable:
"""
Returns the texture of the given type.
"""
if texture_type not in TEXTURE_TYPES:
raise Exception(f"Unknown texture type {texture_type}.")
path = self._importer.get_texture(self._material_index, TEXTURE_TYPES[texture_type])
if path is None:
rv = renpy.display.im.Null()
elif path.startswith("*"):
rv = self._importer.model_data.embedded_textures[path]
else:
rv = renpy.easy.displayable(self._importer.dirname + "/" + path)
return renpy.display.im.render_for_texture(unoptimized_texture(rv), 0, 0, 0, 0)
def wrap_texture(self, d):
"""
Wraps an image file so it can be used as a texture.
"""
d = renpy.easy.displayable(d)
return renpy.display.im.render_for_texture(unoptimized_texture(d), 0, 0, 0, 0)
MeshCallbackType = Callable[[MeshInfo], Render]
class MeshCallback:
textures: list
"""
A callback that's called for each mesh in the model.
A list of displaybles to render and blit as textures.
"""
textures: tuple[str]
"The textures that the callback is interested in."
shaders: Iterable[str]
"""
The shaders to use.
"""
shaders: tuple[str]
"The shaders that the callback is interested in."
def __init__(self, Loader loader, Mesh3 mesh, reverse_matrix: Matrix, forward_matrix: Matrix, int material_index, textures: Iterable[str], shaders: Iterable[str]):
self.mesh = mesh
self.reverse_matrix = reverse_matrix
self.forward_matrix = forward_matrix
self.textures = [ ]
for t in textures:
if isinstance(t, str) and t in TEXTURE_TYPES:
path = loader.get_texture(material_index, TEXTURE_TYPES[t])
if path is None:
d = renpy.display.im.Null()
elif path.startswith("*"):
d = loader.model_data.embedded_textures[path]
else:
d = renpy.easy.displayable(loader.dirname + "/" + path)
else:
d = renpy.easy.displayable(t)
self.textures.append(unoptimized_texture(d))
def __init__(self, textures=( "diffuse", ), shaders=()):
self.textures = textures
self.shaders = shaders
def __call__(self, mesh: MeshInfo) -> None:
"""
Called for each mesh in the model.
"""
rv = renpy.display.render.Render(0, 0)
rv.mesh = mesh.mesh
rv.reverse = mesh.reverse_matrix
rv.forward = mesh.forward_matrix
rv.add_property("texture_wrap", (renpy.uguu.GL_REPEAT, renpy.uguu.GL_REPEAT))
for i in self.shaders:
rv.add_shader(i)
for i in self.textures:
rv.blit(mesh.get_texture(i), (0, 0))
return rv
cdef class Loader:
cdef public str dirname
@@ -239,18 +199,22 @@ cdef class Loader:
cdef public object model_data
"The ModelData object that is being filled in."
cdef public object mesh_callback
"The callback that is called for each mesh."
cdef public bint tangents
"True if tangents should be included in the mesh."
cdef public object textures
"A list of names of shaders to use."
cdef public object shaders
"A shader or list of shaders to use."
def __cinit__(self):
self.importer.SetIOHandler(new RenpyIOSystem())
def load(self, filename: str, mesh_callback, tangents) -> None:
def load(self, model_data: ModelData, filename: str, textures: Iterable[str], shaders: Iterable[str], tangents: bool) -> None:
self.mesh_callback = mesh_callback
self.shaders = shaders
self.textures = textures
self.tangents = tangents
self.dirname = filename.rpartition("/")[0]
@@ -266,7 +230,7 @@ cdef class Loader:
try:
self.model_data = ModelData()
self.model_data = model_data
self.load_textures()
@@ -280,8 +244,9 @@ cdef class Loader:
return self.model_data
finally:
self.shaders = [ ]
self.textures = [ ]
self.model_data = None
self.mesh_callback = None
self.importer.FreeScene()
@@ -415,21 +380,23 @@ cdef class Loader:
triangle += 3
info = MeshInfo()
info._importer = self
info._material_index = mesh.mMaterialIndex
info.mesh = m
info.reverse_matrix = matrix
info.forward_matrix = matrix.inverse()
info = MeshInfo(
self,
m,
matrix,
matrix.inverse(),
mesh.mMaterialIndex,
self.textures,
self.shaders)
r = self.mesh_callback(info)
if r is not None:
self.model_data.mesh_renders.append(r)
self.model_data.mesh_info.append(info)
loader = Loader()
"The loader used to load in imported models."
loader_lock = threading.Lock()
"The lock used to protect the loader."
class AssimpModel(renpy.display.displayable.Displayable):
"""
A displayable that displays a model.
@@ -438,10 +405,19 @@ class AssimpModel(renpy.display.displayable.Displayable):
filename: str
"The filename of the model to display."
textures: Iterable[str]
"The textures to load."
shaders: Iterable[str]
"The shaders to use."
tangents: bool
"True if tangents should be included in the mesh."
def __init__(
self,
filename: str,
callback: MeshCallbackType|None = None,
textures: Iterable[str] = ("diffuse",),
shader: str|tuple[str] = "renpy.texture",
tangents: bool = False):
@@ -453,30 +429,106 @@ class AssimpModel(renpy.display.displayable.Displayable):
else:
shaders = shader
if callback is None:
callback = MeshCallback(textures=textures, shaders=shaders)
self.filename = filename
self.callback = callback
self.textures = textures
self.shaders = shaders
self.tangents = tangents
def load(self):
"""
Loads the AssimpModel, including its meshes and textures.
"""
model_data = cache.get(self)
if model_data is None:
with loader_lock:
model_data = cache[self] = ModelData()
try:
loader.load(model_data, self.filename, self.textures, self.shaders, self.tangents)
except Exception as e:
del cache[self]
raise
for d in (j for i in cache[self].mesh_info for j in i.textures if j):
renpy.display.im.cache.preload_image(d)
return model_data
def render(self, width, height, st, at):
key = id(self)
model_data = cache.get(key)
new_predicted.add(self)
if model_data is None:
model_data = cache[key] = loader.load(self.filename, self.callback, self.tangents)
model_data = self.load()
rv = Render(0, 0)
for cr in model_data.mesh_renders:
rv.blit(cr, (0, 0), focus=False, main=False)
for mi in model_data.mesh_info:
cr = Render(0, 0)
cr.mesh = mi.mesh
cr.reverse = mi.reverse_matrix
cr.forward = mi.forward_matrix
for i in mi.shaders:
cr.add_shader(i)
for i in mi.textures:
cr.blit(renpy.display.im.render_for_texture(i, width, height, st, at), (0, 0))
rv.blit(cr, (0, 0))
rv.add_property("depth", True)
rv.add_property("texture_wrap", (renpy.uguu.GL_REPEAT, renpy.uguu.GL_REPEAT))
return rv
def visit(self):
if self in cache:
return [ j for i in cache[self].mesh_info for j in i.textures ]
else:
return [ ]
def predict_one(self):
new_predicted.add(self)
def __hash__(self):
return id(self)
def __eq__(self, other):
return self is other
def finish_predict():
"""
Called to finish the prediction process.
"""
global predicted
global new_predicted
if new_predicted != predicted:
predicted = new_predicted
new_predicted = set()
renpy.display.im.cache.start_prediction()
def preload():
"""
Called to preload predicted models.
"""
if predicted is None:
return
for i in set(cache.keys()) - predicted:
del cache[i]
for i in predicted:
i.load()
cdef public int assimp_loadable(const char *filename) nogil:
"""