From 978b80f1ccc522c5c6f89334bc74032ce5aef21c Mon Sep 17 00:00:00 2001 From: Joshua Slack Date: Mon, 27 May 2024 13:12:41 -0500 Subject: [PATCH] Add tracking and ability to clean up ShaderPrograms. Also add expired VAO clean up to runtime cleanup code in ContextGarbageCollector. --- .../renderer/material/IShaderUtils.java | 8 +- .../renderer/material/TechniquePass.java | 216 +++++++++++++++++- .../java/com/ardor3d/scenegraph/MeshData.java | 10 + .../visitor/DeleteShaderProgramsVisitor.java | 38 +++ .../ardor3d/util/ContextGarbageCollector.java | 14 +- .../state/lwjgl3/util/Lwjgl3ShaderUtils.java | 8 + 6 files changed, 284 insertions(+), 10 deletions(-) create mode 100644 ardor3d-core/src/main/java/com/ardor3d/scenegraph/visitor/DeleteShaderProgramsVisitor.java diff --git a/ardor3d-core/src/main/java/com/ardor3d/renderer/material/IShaderUtils.java b/ardor3d-core/src/main/java/com/ardor3d/renderer/material/IShaderUtils.java index 3104662f..ceb3c7e2 100644 --- a/ardor3d-core/src/main/java/com/ardor3d/renderer/material/IShaderUtils.java +++ b/ardor3d-core/src/main/java/com/ardor3d/renderer/material/IShaderUtils.java @@ -60,7 +60,7 @@ public interface IShaderUtils { * Attempts to delete a vertex array associated with this mesh data that is relevant to the current * RenderContext. * - * @param ids + * @param data */ void deleteVertexArray(MeshData data); @@ -71,4 +71,10 @@ public interface IShaderUtils { */ void deleteVertexArrays(Collection ids); + /** + * Attempts to delete shader programs with the given ids. Ignores null ids or ids < 1. + * + * @param ids + */ + void deleteShaderPrograms(List ids); } diff --git a/ardor3d-core/src/main/java/com/ardor3d/renderer/material/TechniquePass.java b/ardor3d-core/src/main/java/com/ardor3d/renderer/material/TechniquePass.java index e4bc750b..de3cb30e 100644 --- a/ardor3d-core/src/main/java/com/ardor3d/renderer/material/TechniquePass.java +++ b/ardor3d-core/src/main/java/com/ardor3d/renderer/material/TechniquePass.java @@ -13,17 +13,14 @@ import java.io.*; import java.lang.ref.ReferenceQueue; import java.nio.Buffer; -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import com.ardor3d.buffer.AbstractBufferData; import com.ardor3d.light.LightManager; import com.ardor3d.renderer.ContextManager; import com.ardor3d.renderer.RenderContext; import com.ardor3d.renderer.Renderer; +import com.ardor3d.renderer.RendererCallable; import com.ardor3d.renderer.material.uniform.Ardor3dStateProperty; import com.ardor3d.renderer.material.uniform.UniformRef; import com.ardor3d.renderer.material.uniform.UniformType; @@ -33,12 +30,18 @@ import com.ardor3d.scenegraph.SceneIndexer; import com.ardor3d.util.Ardor3dException; import com.ardor3d.util.Constants; +import com.ardor3d.util.GameTaskQueueManager; +import com.ardor3d.util.collection.Multimap; +import com.ardor3d.util.collection.SimpleMultimap; import com.ardor3d.util.gc.ContextValueReference; public class TechniquePass { /** Name of this pass - optional, useful for debug, etc. */ protected String _name; + private static final Map _identityCache = Collections.synchronizedMap(new WeakHashMap<>()); + private static final Object STATIC_REF = new Object(); + /** Our shaders, mapped by their type. */ protected Map> _shaders = new EnumMap<>(ShaderType.class); @@ -54,9 +57,29 @@ public class TechniquePass { protected Map _cachedLocations = new IdentityHashMap<>(); + public TechniquePass() { + synchronized (_identityCache) { + _identityCache.put(this, STATIC_REF); + } + } + + /** + * @param context the OpenGL context to get our id for. + * @return the program id of a shader program in the given context. If the program is not found in the given + * * context rep, 0 is returned. + */ public int getProgramId(final RenderContext context) { + return getProgramIdByRef(context.getSharableContextRef()); + } + + /** + * @param contextRef the reference to a shared GL context to get our id for. + * @return the program id of a shader program in the given context. If the program is not found in the given + * context rep, 0 is returned. + */ + public int getProgramIdByRef(final RenderContext.RenderContextRef contextRef) { if (_shaderIdCache != null) { - final Integer id = _shaderIdCache.getValue(context.getSharableContextRef()); + final Integer id = _shaderIdCache.getValue(contextRef); if (id != null) { return id; } @@ -75,6 +98,187 @@ public void setProgramId(final RenderContext context, final int id) { _shaderIdCache.put(context.getSharableContextRef(), id); } + /** + * Clean all tracked Shader Programs from the hardware, using the given utility object to do the work immediately, + * if given. If not, we will delete in the next execution of the appropriate context's game task + * render queue. + * + * @param utils + * the util class to use. If null, execution will not occur immediately. + */ + public static void cleanAllPrograms(final IShaderUtils utils) { + final Multimap idMap = new SimpleMultimap<>(); + + // gather up expired shader program ids... these don't exist in our cache + gatherGCdIds(idMap); + + Set keySetCopy; + synchronized (_identityCache) { + keySetCopy = new HashSet<>(_identityCache.keySet()); + } + + // Walk through the cached items and delete those too. + for (final TechniquePass pass : keySetCopy) { + if (pass._shaderIdCache != null) { + if (Constants.useMultipleContexts) { + final Set contextObjects = pass._shaderIdCache.getContextRefs(); + for (final RenderContext.RenderContextRef o : contextObjects) { + // Add id to map + idMap.put(o, pass.getProgramIdByRef(o)); + } + } else { + idMap.put(ContextManager.getCurrentContext().getSharableContextRef(), pass.getProgramIdByRef(null)); + } + pass._shaderIdCache.clear(); + } + } + + // send to be deleted (perhaps on next render.) + handleProgramDelete(utils, idMap); + } + + /** + * Clean all tracked Shader Programs associated with the given RenderContext from the hardware, using the given utility object to do the work immediately, + * if given. If not, we will delete in the next execution of the appropriate context's game task + * render queue. + * + * @param utils the util class to use. If null, execution will not occur immediately. + * @param context the context to clean programs for. + */ + public static void cleanAllPrograms(final IShaderUtils utils, final RenderContext context) { + final Multimap idMap = new SimpleMultimap<>(); + + // gather up expired vbos... these don't exist in our cache + gatherGCdIds(idMap); + + final RenderContext.RenderContextRef glRef = context.getSharableContextRef(); + + Set keySetCopy; + synchronized (_identityCache) { + keySetCopy = new HashSet<>(_identityCache.keySet()); + } + + // Walk through the cached items and delete those too. + for (final TechniquePass pass : keySetCopy) { + // only worry about buffers that have received ids. + if (pass._shaderIdCache != null) { + final Integer id = pass._shaderIdCache.removeValue(glRef); + if (id != null && id != 0) { + idMap.put(context.getSharableContextRef(), id); + } + } + } + + // send to be deleted (perhaps on next render.) + handleProgramDelete(utils, idMap); + } + + /** + * Clean tracked Shader Programs from the hardware for this TechniquePass, using the given utility object to do the work immediately, + * if given. If not, we will delete in the next execution of the appropriate context's game task + * render queue. + * + * @param utils the util class to use. If null, execution will not occur immediately. + */ + public void cleanProgram(final IShaderUtils utils) { + if (_shaderIdCache != null) { + final Multimap idMap = new SimpleMultimap<>(); + + if (Constants.useMultipleContexts) { + final Set contextObjects = _shaderIdCache.getContextRefs(); + for (final RenderContext.RenderContextRef o : contextObjects) { + // Add id to map + idMap.put(o, getProgramIdByRef(o)); + } + } else { + idMap.put(ContextManager.getCurrentContext().getSharableContextRef(), getProgramIdByRef(null)); + } + + // clear out the cache + _shaderIdCache.clear(); + + // send to be deleted (perhaps on next render.) + handleProgramDelete(utils, idMap); + } + } + + /** + * Clean any tracked, expired Shader Programs from the hardware, using the given utility object to do the work immediately, + * if given. If not, we will delete in the next execution of the appropriate context's game task + * render queue. + *

+ * A Shader Program is considered expired if it has been garbage collected by Java. + * + * @param utils the util class to use. If null, execution will not occur immediately. + */ + public static void cleanExpiredPrograms(final IShaderUtils utils) { + // gather up expired vbos... + final Multimap idMap = gatherGCdIds(null); + + if (idMap != null) { + // send to be deleted (perhaps on next render.) + handleProgramDelete(utils, idMap); + } + } + + @SuppressWarnings("unchecked") + private static Multimap gatherGCdIds(Multimap store) { + // Pull all expired shader programs from ref queue and add to an id multimap. + ContextValueReference ref; + while ((ref = (ContextValueReference) _shaderRefQueue.poll()) != null) { + if (Constants.useMultipleContexts) { + final Set renderRefs = ref.getContextRefs(); + for (final RenderContext.RenderContextRef renderRef : renderRefs) { + // Add id to map + final Integer id = ref.getValue(renderRef); + if (id != null) { + if (store == null) { // lazy init + store = new SimpleMultimap<>(); + } + store.put(renderRef, id); + } + } + } else { + final Integer id = ref.getValue(null); + if (id != null) { + if (store == null) { // lazy init + store = new SimpleMultimap<>(); + } + store.put(ContextManager.getCurrentContext().getSharableContextRef(), id); + } + } + ref.clear(); + } + + return store; + } + + private static void handleProgramDelete(final IShaderUtils utils, final Multimap idMap) { + RenderContext.RenderContextRef currentSharableRef = null; + // Grab the current context, if any. + if (utils != null && ContextManager.getCurrentContext() != null) { + currentSharableRef = ContextManager.getCurrentContext().getSharableContextRef(); + } + // For each affected context... + for (final RenderContext.RenderContextRef sharableRef : idMap.keySet()) { + // If we have a deleter and the context is current, immediately delete + if (utils != null && sharableRef.equals(currentSharableRef)) { + utils.deleteShaderPrograms(idMap.values(sharableRef)); + } + // Otherwise, add a delete request to that context's render task queue. + else { + GameTaskQueueManager.getManager(ContextManager.getContextForSharableRef(sharableRef)) + .render(new RendererCallable() { + @Override + public Void call() { + getRenderer().getShaderUtils().deleteShaderPrograms(idMap.values(sharableRef)); + return null; + } + }); + } + } + } + public void setName(final String name) { _name = name; } public String getName() { return _name; } diff --git a/ardor3d-core/src/main/java/com/ardor3d/scenegraph/MeshData.java b/ardor3d-core/src/main/java/com/ardor3d/scenegraph/MeshData.java index 015d56ff..be790c5d 100644 --- a/ardor3d-core/src/main/java/com/ardor3d/scenegraph/MeshData.java +++ b/ardor3d-core/src/main/java/com/ardor3d/scenegraph/MeshData.java @@ -1191,6 +1191,16 @@ public void read(final InputCapsule capsule) throws IOException { updatePrimitiveCounts(); } + public static void cleanExpiredVertexArrays(final IShaderUtils utils) { + // gather up expired vertex arrays... + final Multimap idMap = gatherGCdIds(null); + + if (idMap != null) { + // send to be deleted (perhaps on next render.) + handleVAODelete(utils, idMap); + } + } + public static void cleanAllVertexArrays(final IShaderUtils utils) { final Multimap idMap = new SimpleMultimap<>(); diff --git a/ardor3d-core/src/main/java/com/ardor3d/scenegraph/visitor/DeleteShaderProgramsVisitor.java b/ardor3d-core/src/main/java/com/ardor3d/scenegraph/visitor/DeleteShaderProgramsVisitor.java new file mode 100644 index 00000000..31ec2ebe --- /dev/null +++ b/ardor3d-core/src/main/java/com/ardor3d/scenegraph/visitor/DeleteShaderProgramsVisitor.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2008-2024 Bird Dog Games, Inc. + * + * This file is part of Ardor3D. + * + * Ardor3D is free software: you can redistribute it and/or modify it + * under the terms of its license which may be found in the accompanying + * LICENSE file or at . + */ + +package com.ardor3d.scenegraph.visitor; + +import com.ardor3d.renderer.material.IShaderUtils; +import com.ardor3d.scenegraph.Mesh; +import com.ardor3d.scenegraph.Spatial; + +/** + * DeleteShaderProgramsVisitor is a visitor that will clear all uploaded + * shader programs associated with RenderMaterials in a given part of the scene graph. + */ +public class DeleteShaderProgramsVisitor implements Visitor { + final IShaderUtils _utils; + + public DeleteShaderProgramsVisitor(final IShaderUtils utils) { + _utils = utils; + } + + @Override + public void visit(final Spatial spatial) { + if (spatial instanceof Mesh mesh) { + var material = mesh.getRenderMaterial(); + if (material == null) return; + material.getTechniques().forEach(technique -> + technique.getPasses().forEach(pass -> + pass.cleanProgram(_utils))); + } + } +} diff --git a/ardor3d-core/src/main/java/com/ardor3d/util/ContextGarbageCollector.java b/ardor3d-core/src/main/java/com/ardor3d/util/ContextGarbageCollector.java index 2567bc29..eba9136e 100644 --- a/ardor3d-core/src/main/java/com/ardor3d/util/ContextGarbageCollector.java +++ b/ardor3d-core/src/main/java/com/ardor3d/util/ContextGarbageCollector.java @@ -12,6 +12,7 @@ import com.ardor3d.buffer.AbstractBufferData; import com.ardor3d.renderer.Renderer; +import com.ardor3d.renderer.material.TechniquePass; import com.ardor3d.scenegraph.MeshData; public class ContextGarbageCollector { @@ -30,7 +31,11 @@ private ContextGarbageCollector() {} */ public static void doRuntimeCleanup(final Renderer renderer) { TextureManager.cleanExpiredTextures(renderer.getTextureUtils(), null); - AbstractBufferData.cleanExpiredVBOs(renderer.getShaderUtils()); + + final var shaderUtils = renderer.getShaderUtils(); + AbstractBufferData.cleanExpiredVBOs(shaderUtils); + MeshData.cleanExpiredVertexArrays(shaderUtils); + TechniquePass.cleanExpiredPrograms(shaderUtils); } /** @@ -46,7 +51,10 @@ public static void doRuntimeCleanup(final Renderer renderer) { */ public static void doFinalCleanup(final Renderer renderer) { TextureManager.cleanAllTextures(renderer.getTextureUtils(), null); - AbstractBufferData.cleanAllBuffers(renderer.getShaderUtils()); - MeshData.cleanAllVertexArrays(renderer.getShaderUtils()); + + final var shaderUtils = renderer.getShaderUtils(); + AbstractBufferData.cleanAllBuffers(shaderUtils); + MeshData.cleanAllVertexArrays(shaderUtils); + TechniquePass.cleanAllPrograms(shaderUtils); } } diff --git a/ardor3d-lwjgl3/src/main/java/com/ardor3d/scene/state/lwjgl3/util/Lwjgl3ShaderUtils.java b/ardor3d-lwjgl3/src/main/java/com/ardor3d/scene/state/lwjgl3/util/Lwjgl3ShaderUtils.java index 633adea3..a72d52e4 100644 --- a/ardor3d-lwjgl3/src/main/java/com/ardor3d/scene/state/lwjgl3/util/Lwjgl3ShaderUtils.java +++ b/ardor3d-lwjgl3/src/main/java/com/ardor3d/scene/state/lwjgl3/util/Lwjgl3ShaderUtils.java @@ -787,4 +787,12 @@ public void deleteVertexArrays(final Collection ids) { } } + @Override + public void deleteShaderPrograms(final List ids) { + for (final Integer i : ids) { + if (i != null && i != 0) { + GL20C.glDeleteProgram(i); + } + } + } }