diff --git a/jme3-core/src/main/java/com/jme3/opencl/Program.java b/jme3-core/src/main/java/com/jme3/opencl/Program.java index c3cbb89c0..a98cf2c02 100644 --- a/jme3-core/src/main/java/com/jme3/opencl/Program.java +++ b/jme3-core/src/main/java/com/jme3/opencl/Program.java @@ -71,7 +71,7 @@ public abstract class Program extends AbstractOpenCLObject { * @see #build(java.lang.String) */ public void build() throws KernelCompilationException { - build("", null); + build("", (Device[]) null); } /** diff --git a/jme3-core/src/main/java/com/jme3/opencl/ProgramCache.java b/jme3-core/src/main/java/com/jme3/opencl/ProgramCache.java new file mode 100644 index 000000000..0356cf3d7 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/opencl/ProgramCache.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2009-2016 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.opencl; + +import com.jme3.system.JmeSystem; +import com.jme3.util.BufferUtils; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implements a simple cache system for program objects. + * The program objects are saved persistently with {@link #saveToCache(java.lang.String, com.jme3.opencl.Program) }. + * On the next run, the stored programs can then be loaded + * with {@link #loadFromCache(java.lang.String, java.lang.String) }. + *
+ * The programs are identified by a unique id. The following format is recommended: + * {@code id = .}. + * + * @author shaman + */ +public class ProgramCache { + private static final Logger LOG = Logger.getLogger(ProgramCache.class.getName()); + private static final String FILE_EXTENSION = ".clbin"; + + private final Context context; + private final Device device; + private final File tmpFolder; + + /** + * Creates a new program cache associated with the specified context and + * devices. + * The cached programs are built against the specified device and also + * only the binaries linked to that device are stored. + * @param context the OpenCL context + * @param device the OpenCL device + */ + public ProgramCache(Context context, Device device) { + this.context = context; + this.device = device; + if (JmeSystem.isLowPermissions()) { + tmpFolder = null; + } else { + tmpFolder = JmeSystem.getStorageFolder(); + } + } + + protected String getCleanFileName(String id) { + //http://stackoverflow.com/a/35591188/4053176 + return id.replaceAll("[^a-zA-Z0-9.-]", "") + FILE_EXTENSION; + } + + /** + * Creates a new program cache using the first device from the specified + * context. + * @param context the context + * @see #ProgramCache(com.jme3.opencl.Context, com.jme3.opencl.Device) + */ + public ProgramCache(Context context) { + this(context, context.getDevices().get(0)); + } + + /** + * Loads the program from the cache and builds it against the current device. + * You can pass additional build arguments with the parameter {@code buildArgs}. + *

+ * The cached program is identified by the specified id. + * This id must be unique, otherwise collisions within the cache occur. + * Therefore, the following naming schema is recommended: + * {@code id = .}. + *

+ * If the program can't be loaded, built or any other exception happened, + * {@code null} is returned. + * + * @param id the unique identifier of this program + * @param buildArgs additional build arguments, can be {@code null} + * @return the loaded and built program, or {@code null} + * @see #saveToCache(java.lang.String, com.jme3.opencl.Program) + */ + public Program loadFromCache(String id, String buildArgs) { + if (tmpFolder == null) { + return null; //low permissions + } + //get file + File file = new File(tmpFolder, getCleanFileName(id)); + if (!file.exists()) { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Cache file {0} does not exist", file.getAbsolutePath()); + } + return null; + } + //load from file + ByteBuffer bb; + try { + byte[] bytes = Files.readAllBytes(file.toPath()); + bb = BufferUtils.createByteBuffer(bytes); + } catch (IOException ex) { + LOG.log(Level.FINE, "Unable to read cache file", ex); + return null; + } + //create program + Program program; + try { + program = context.createProgramFromBinary(bb, device); + } catch (OpenCLException ex) { + LOG.log(Level.FINE, "Unable to create program from binary", ex); + return null; + } + //build program + try { + program.build(buildArgs, device); + } catch (OpenCLException ex) { + LOG.log(Level.FINE, "Unable to build program", ex); + return null; + } + //done + return program; + } + + /** + * Calls {@link #loadFromCache(java.lang.String, java.lang.String) } + * with the additional build arguments set to {@code ""}. + * @param id a unique identifier of the program + * @return the loaded and built program or {@code null} if this + * program could not be loaded from the cache + * @see #loadFromCache(java.lang.String, java.lang.String) + */ + public Program loadFromCache(String id) { + return loadFromCache(id, ""); + } + + /** + * Saves the specified program in the cache. + * The parameter {@code id} denotes the name of the program. Under this id, + * the program is then loaded again by {@link #loadFromCache(java.lang.String, java.lang.String) }. + *
+ * The id must be unique, otherwise collisions within the cache occur. + * Therefore, the following naming schema is recommended: + * {@code id = .}. + * + * @param id the program id + * @param program the program to store in the cache + */ + public void saveToCache(String id, Program program) { + if (tmpFolder == null) { + return; //low permissions + } + //get file + File file = new File(tmpFolder, getCleanFileName(id)); + //get binaries + ByteBuffer bb; + try { + bb = program.getBinary(device); + } catch (UnsupportedOperationException | OpenCLException ex) { + LOG.log(Level.WARNING, "Unable to retrieve the program binaries", ex); + return; + } + byte[] bytes = new byte[bb.remaining()]; + bb.get(bytes); + //save + try { + Files.write(file.toPath(), bytes); + } catch (IOException ex) { + LOG.log(Level.WARNING, "Unable to save program binaries to the cache", ex); + } + } + + /** + * Clears the cache. + * All saved program binaries are deleted. + */ + public void clearCache() { + if (tmpFolder == null) { + return; //low permissions + } + for (File file : tmpFolder.listFiles()) { + if (file.isFile() && file.getName().endsWith(FILE_EXTENSION)) { + file.delete(); + } + } + } +} diff --git a/jme3-examples/src/main/java/jme3test/opencl/TestWriteToTexture.java b/jme3-examples/src/main/java/jme3test/opencl/TestWriteToTexture.java index 5f3143832..bb407893c 100644 --- a/jme3-examples/src/main/java/jme3test/opencl/TestWriteToTexture.java +++ b/jme3-examples/src/main/java/jme3test/opencl/TestWriteToTexture.java @@ -49,6 +49,8 @@ import java.util.logging.Logger; * This test class tests the capability to write to a GL texture from OpenCL. * Move the mouse around while pressing the left mouse key to modify the fractal. * + * In addition, this test shows how to use {@link ProgramCache}. + * * @author shaman */ public class TestWriteToTexture extends SimpleApplication implements AnalogListener, ActionListener { @@ -59,6 +61,7 @@ public class TestWriteToTexture extends SimpleApplication implements AnalogListe private int initCounter; private Context clContext; private CommandQueue clQueue; + private ProgramCache programCache; private Kernel kernel; private Vector2f C; private Image texCL; @@ -121,9 +124,16 @@ public class TestWriteToTexture extends SimpleApplication implements AnalogListe clContext = context.getOpenCLContext(); clQueue = clContext.createQueue(); clQueue.register(); + programCache = new ProgramCache(clContext); //create kernel - Program program = clContext.createProgramFromSourceFiles(assetManager, "jme3test/opencl/JuliaSet.cl"); - program.build(); + String cacheID = getClass().getName()+".Julia"; + Program program = programCache.loadFromCache(cacheID); + if (program == null) { + LOG.info("Program not loaded from cache, create from sources instead"); + program = clContext.createProgramFromSourceFiles(assetManager, "jme3test/opencl/JuliaSet.cl"); + program.build(); + programCache.saveToCache(cacheID, program); + } program.register(); kernel = program.createKernel("JuliaSet"); kernel.register();