From 19ec835c693c5b0cc3bac63a7be2febdab149c8a Mon Sep 17 00:00:00 2001 From: Gary Frost <frost.gary@gmail.com> Date: Mon, 13 Feb 2012 20:53:05 +0000 Subject: [PATCH] Merge of MultiDimSupport and SupportGlobalLocalMemory branches. --- build.xml | 1 + com.amd.aparapi.jni/src/cpp/aparapi.cpp | 2253 +++++++++-------- com.amd.aparapi/build.xml | 2 +- .../src/java/com/amd/aparapi/ClassModel.java | 4 +- .../src/java/com/amd/aparapi/Config.java | 1 + .../com/amd/aparapi/DeprecatedException.java | 46 + .../src/java/com/amd/aparapi/Kernel.java | 306 ++- .../java/com/amd/aparapi/KernelRunner.java | 529 ++-- .../java/com/amd/aparapi/KernelWriter.java | 74 +- .../src/java/com/amd/aparapi/MethodModel.java | 2 - .../src/java/com/amd/aparapi/Range.java | 392 +++ .../java/com/amd/aparapi/RangeException.java | 46 + .../amd/aparapi/examples/effects/Main.java | 7 +- examples/nbody/local.bat | 14 + .../com/amd/aparapi/examples/nbody/Local.java | 356 +++ .../com/amd/aparapi/examples/nbody/Main.java | 66 +- samples/blackscholes/.classpath | 8 + samples/blackscholes/.project | 17 + .../aparapi/samples/blackscholes/Main.java | 10 +- samples/convolution/.classpath | 8 + samples/convolution/.project | 17 + samples/convolution/build.xml | 20 + samples/convolution/conv.bat | 6 + .../amd/aparapi/sample/convolution/Main.java | 234 ++ .../sample/convolution/Test12x4_4x2.java | 496 ++++ samples/convolution/testcard.jpg | Bin 0 -> 64477 bytes .../src/com/amd/aparapi/sample/life/Main.java | 7 +- samples/mandel/mandel2D.bat | 7 + samples/mandel/mandel2D.sh | 5 + .../com/amd/aparapi/sample/mandel/Main.java | 8 +- .../com/amd/aparapi/sample/mandel/Main2D.java | 277 ++ .../com/amd/aparapi/sample/squares/Main.java | 4 +- test/codegen/build.xml | 33 +- 33 files changed, 3872 insertions(+), 1384 deletions(-) create mode 100644 com.amd.aparapi/src/java/com/amd/aparapi/DeprecatedException.java create mode 100644 com.amd.aparapi/src/java/com/amd/aparapi/Range.java create mode 100644 com.amd.aparapi/src/java/com/amd/aparapi/RangeException.java create mode 100644 examples/nbody/local.bat create mode 100644 examples/nbody/src/com/amd/aparapi/examples/nbody/Local.java create mode 100644 samples/blackscholes/.classpath create mode 100644 samples/blackscholes/.project create mode 100644 samples/convolution/.classpath create mode 100644 samples/convolution/.project create mode 100644 samples/convolution/build.xml create mode 100644 samples/convolution/conv.bat create mode 100644 samples/convolution/src/com/amd/aparapi/sample/convolution/Main.java create mode 100644 samples/convolution/src/com/amd/aparapi/sample/convolution/Test12x4_4x2.java create mode 100644 samples/convolution/testcard.jpg create mode 100644 samples/mandel/mandel2D.bat create mode 100644 samples/mandel/mandel2D.sh create mode 100644 samples/mandel/src/com/amd/aparapi/sample/mandel/Main2D.java diff --git a/build.xml b/build.xml index ee646ad1..3b19d813 100644 --- a/build.xml +++ b/build.xml @@ -35,6 +35,7 @@ <fileset dir="test" includes="*/build.xml"/> </subant> <delete dir="examples\nbody\jogamp"/> <!-- we handle the jogamp delete here, save downloading each build --> + <delete file="test\codegen\junit-4.10.jar"/> <!-- we handle the junit delete here, save downloading each build --> <ant dir="com.amd.aparapi.jni" target="clean"/> <ant dir="com.amd.aparapi" target="clean"/> </target> diff --git a/com.amd.aparapi.jni/src/cpp/aparapi.cpp b/com.amd.aparapi.jni/src/cpp/aparapi.cpp index 29fa4c27..4e9c4b31 100644 --- a/com.amd.aparapi.jni/src/cpp/aparapi.cpp +++ b/com.amd.aparapi.jni/src/cpp/aparapi.cpp @@ -1,40 +1,40 @@ /* -Copyright (c) 2010-2011, Advanced Micro Devices, Inc. -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 the copyright holder 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 HOLDER 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. - -If you use the software (in whole or in part), you shall adhere to all applicable U.S., European, and other export -laws, including but not limited to the U.S. Export Administration Regulations ("EAR"), (15 C.F.R. Sections 730 through -774), and E.U. Council Regulation (EC) No 1334/2000 of 22 June 2000. Further, pursuant to Section 740.6 of the EAR, -you hereby certify that, except pursuant to a license granted by the United States Department of Commerce Bureau of -Industry and Security or as otherwise permitted pursuant to a License Exception under the U.S. Export Administration -Regulations ("EAR"), you will not (1) export, re-export or release to a national of a country in Country Groups D:1, -E:1 or E:2 any restricted technology, software, or source code you receive hereunder, or (2) export to Country Groups -D:1, E:1 or E:2 the direct product of such technology or software, if such foreign produced direct product is subject -to national security controls as identified on the Commerce Control List (currently found in Supplement 1 to Part 774 -of EAR). For the most current Country Group listings, or for additional information about the EAR or your obligations -under those regulations, please refer to the U.S. Bureau of Industry and Security’s website at http://www.bis.doc.gov/. - -*/ + Copyright (c) 2010-2011, Advanced Micro Devices, Inc. + 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 the copyright holder 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 HOLDER 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. + + If you use the software (in whole or in part), you shall adhere to all applicable U.S., European, and other export + laws, including but not limited to the U.S. Export Administration Regulations ("EAR"), (15 C.F.R. Sections 730 + through 774), and E.U. Council Regulation (EC) No 1334/2000 of 22 June 2000. Further, pursuant to Section 740.6 of + the EAR, you hereby certify that, except pursuant to a license granted by the United States Department of Commerce + Bureau of Industry and Security or as otherwise permitted pursuant to a License Exception under the U.S. Export + Administration Regulations ("EAR"), you will not (1) export, re-export or release to a national of a country in + Country Groups D:1, E:1 or E:2 any restricted technology, software, or source code you receive hereunder, or (2) + export to Country Groups D:1, E:1 or E:2 the direct product of such technology or software, if such foreign produced + direct product is subject to national security controls as identified on the Commerce Control List (currently + found in Supplement 1 to Part 774 of EAR). For the most current Country Group listings, or for additional + information about the EAR or your obligations under those regulations, please refer to the U.S. Bureau of Industry + and Security’s website at http://www.bis.doc.gov/. + */ #include <stdio.h> #include <stdlib.h> @@ -110,6 +110,10 @@ MicrosecondTimer timer; #include "com_amd_aparapi_KernelRunner.h" +#define CHECK_NO_RETURN(condition, msg) if(condition){\ + fprintf(stderr, "!!!!!!! %s failed !!!!!!!\n", msg);\ +} + #define CHECK(condition, msg) if(condition){\ fprintf(stderr, "!!!!!!! %s failed !!!!!!!\n", msg);\ return 0;\ @@ -127,26 +131,9 @@ MicrosecondTimer timer; #define PRINT_CL_ERR(status, msg) fprintf(stderr, "!!!!!!! %s failed %s\n", msg, CLErrString(status)); -#define ASSERT_FIELD(id) CHECK(id##FieldID == 0, "No such field as " #id) - -#define GET_DEV_INFO(deviceId, param, val, format){\ - status = clGetDeviceInfo(deviceId, param, sizeof(val), &(val), NULL);\ - ASSERT_CL_NO_RETURN( "clGetDeviceInfo().");\ - /*fprintf(stderr, #param " " format " \n", val);*/ \ -} - +#define ASSERT_FIELD(id) CHECK_NO_RETURN(id##FieldID == 0, "No such field as " #id) -jfieldID typeFieldID; -jfieldID isStaticFieldID; -jfieldID nameFieldID; -jfieldID javaArrayFieldID; -jfieldID bytesPerLocalSizeFieldID; -jfieldID sizeInBytesFieldID; -jfieldID numElementsFieldID; -// we rely on these being 0 initially to detect whether we have cached the above fieldId's -jclass clazz = (jclass)0; -jclass argClazz = (jclass)0; static const char *CLErrString(cl_int status) { static struct { cl_int code; const char *msg; } error_table[] = { @@ -214,7 +201,90 @@ static const char *CLErrString(cl_int status) { #endif return unknown; } +class Range{ + public: + static jclass rangeClazz; + static jfieldID globalSize_0_FieldID; + static jfieldID globalSize_1_FieldID; + static jfieldID globalSize_2_FieldID; + static jfieldID localSize_0_FieldID; + static jfieldID localSize_1_FieldID; + static jfieldID localSize_2_FieldID; + static jfieldID dimsFieldID; + static jfieldID localIsDerivedFieldID; + jobject range; + cl_int dims; + size_t *offsets; + size_t *globalDims; + size_t *localDims; + jboolean localIsDerived; + Range(JNIEnv *jenv, jobject range): + range(range), + dims(0), + offsets(NULL), + globalDims(NULL), + localDims(NULL){ + if (rangeClazz ==NULL){ + jclass rangeClazz = jenv->GetObjectClass(range); + globalSize_0_FieldID = jenv->GetFieldID(rangeClazz, "globalSize_0", "I"); ASSERT_FIELD(globalSize_0_); + globalSize_1_FieldID = jenv->GetFieldID(rangeClazz, "globalSize_1", "I"); ASSERT_FIELD(globalSize_1_); + globalSize_2_FieldID = jenv->GetFieldID(rangeClazz, "globalSize_2", "I"); ASSERT_FIELD(globalSize_2_); + localSize_0_FieldID = jenv->GetFieldID(rangeClazz, "localSize_0", "I"); ASSERT_FIELD(localSize_0_); + localSize_1_FieldID = jenv->GetFieldID(rangeClazz, "localSize_1", "I"); ASSERT_FIELD(localSize_1_); + localSize_2_FieldID = jenv->GetFieldID(rangeClazz, "localSize_2", "I"); ASSERT_FIELD(localSize_2_); + dimsFieldID = jenv->GetFieldID(rangeClazz, "dims", "I"); ASSERT_FIELD(dims); + localIsDerivedFieldID = jenv->GetFieldID(rangeClazz, "localIsDerived", "Z"); ASSERT_FIELD(localIsDerived); + } + dims = jenv->GetIntField(range, dimsFieldID); + localIsDerived = jenv->GetBooleanField(range, localIsDerivedFieldID); + if (dims >0){ + //fprintf(stderr, "native range dims == %d\n", dims); + offsets = new size_t[dims]; + globalDims = new size_t[dims]; + localDims = new size_t[dims]; + offsets[0]= 0; + localDims[0]= jenv->GetIntField(range, localSize_0_FieldID); + //fprintf(stderr, "native range localSize_0 == %d\n", localDims[0]); + globalDims[0]= jenv->GetIntField(range, globalSize_0_FieldID); + //fprintf(stderr, "native range globalSize_0 == %d\n", globalDims[0]); + if (dims >1){ + offsets[1]= 0; + localDims[1]= jenv->GetIntField(range, localSize_1_FieldID); + //fprintf(stderr, "native range localSize_1 == %d\n", localDims[1]); + globalDims[1]= jenv->GetIntField(range, globalSize_1_FieldID); + //fprintf(stderr, "native range globalSize_1 == %d\n", globalDims[1]); + if (dims >2){ + offsets[2]= 0; + localDims[2]= jenv->GetIntField(range, localSize_2_FieldID); + //fprintf(stderr, "native range localSize_2 == %d\n", localDims[2]); + globalDims[2]= jenv->GetIntField(range, globalSize_2_FieldID); + //fprintf(stderr, "native range globalSize_2 == %d\n", globalDims[2]); + } + } + } + } + ~Range(){ + if (offsets!= NULL){ + delete offsets; + } + if (globalDims!= NULL){ + delete globalDims; + } + if (localDims!= NULL){ + delete localDims; + } + } +}; +jclass Range::rangeClazz = (jclass)0; +jfieldID Range::globalSize_0_FieldID=0; +jfieldID Range::globalSize_1_FieldID=0; +jfieldID Range::globalSize_2_FieldID=0; +jfieldID Range::localSize_0_FieldID=0; +jfieldID Range::localSize_1_FieldID=0; +jfieldID Range::localSize_2_FieldID=0; +jfieldID Range::dimsFieldID=0; +jfieldID Range::localIsDerivedFieldID=0; class ProfileInfo{ public: @@ -243,13 +313,22 @@ class KernelArgRef{ class JNIContext ; // forward reference class KernelArg{ + private: + static jclass argClazz; + static jfieldID nameFieldID; + static jfieldID typeFieldID; + static jfieldID isStaticFieldID; + static jfieldID sizeInBytesFieldID; + static jfieldID numElementsFieldID; public: + static jfieldID javaArrayFieldID; + jobject argObj; char *name; // used for debugging printfs jfieldID fieldID; // The field that this arg represents in the kernel (java), used only for primitive updates jint type; // a bit mask determining the type of this arg jboolean isStatic; // A flag indicating if the value is static jint sizeInBytes; // bytes in the array or directBuf - jobject javaArg; // global reference to the corresponding java KernelArg + jobject javaArg; // global reference to the corresponding java KernelArg object union{ cl_char c; cl_double d; @@ -259,6 +338,29 @@ class KernelArg{ KernelArgRef ref; } value; + KernelArg(JNIEnv *jenv, jobject argObj): + argObj(argObj){ + javaArg = jenv->NewGlobalRef(argObj); // save a global ref to the java Arg Object + if (argClazz == 0){ + jclass c = jenv->GetObjectClass(argObj); + nameFieldID = jenv->GetFieldID(c, "name", "Ljava/lang/String;"); ASSERT_FIELD(name); + typeFieldID = jenv->GetFieldID(c, "type", "I"); ASSERT_FIELD(type); + isStaticFieldID = jenv->GetFieldID(c, "isStatic", "Z"); ASSERT_FIELD(isStatic); + javaArrayFieldID = jenv->GetFieldID(c, "javaArray", "Ljava/lang/Object;"); ASSERT_FIELD(javaArray); + sizeInBytesFieldID = jenv->GetFieldID(c, "sizeInBytes", "I"); ASSERT_FIELD(sizeInBytes); + numElementsFieldID = jenv->GetFieldID(c, "numElements", "I"); ASSERT_FIELD(numElements); + } + type = jenv->GetIntField(argObj, typeFieldID); + isStatic = jenv->GetBooleanField(argObj, isStaticFieldID); + jstring nameString = (jstring)jenv->GetObjectField(argObj, nameFieldID); + const char *nameChars = jenv->GetStringUTFChars(nameString, NULL); + name=strdup(nameChars); + jenv->ReleaseStringUTFChars(nameString, nameChars); + } + + ~KernelArg(){ + } + void unpinAbort(JNIEnv *jenv){ jenv->ReleasePrimitiveArrayCritical((jarray)value.ref.javaArray, value.ref.addr,JNI_ABORT); } @@ -342,9 +444,6 @@ class KernelArg{ int isAparapiBufHasArray(){ return (type&com_amd_aparapi_KernelRunner_ARG_APARAPI_BUF_HAS_ARRAY); } - int isAparapiBufIsDirect(){ - return (type&com_amd_aparapi_KernelRunner_ARG_APARAPI_BUF_IS_DIRECT); - } int isBackedByArray(){ return ( (isArray() && isGlobal()) || ((isGlobal() || isConstant()) && isAparapiBufHasArray())); } @@ -354,8 +453,27 @@ class KernelArg{ int mustWriteBuffer(){ return ((isImplicit()&&isRead()&&!isConstant())||(isExplicit()&&isExplicitWrite())); } - + void syncType(JNIEnv* jenv){ + type = jenv->GetIntField(javaArg, typeFieldID); + } + void syncSizeInBytes(JNIEnv* jenv){ + sizeInBytes = jenv->GetIntField(javaArg, sizeInBytesFieldID); + } + void syncJavaArrayLength(JNIEnv* jenv){ + value.ref.javaArrayLength = jenv->GetIntField(javaArg, numElementsFieldID); + } + void clearExplicitBufferBit(JNIEnv* jenv){ + type &= ~com_amd_aparapi_KernelRunner_ARG_EXPLICIT_WRITE; + jenv->SetIntField(javaArg, typeFieldID,type ); + } }; +jclass KernelArg::argClazz=(jclass)0; +jfieldID KernelArg::nameFieldID=0; +jfieldID KernelArg::typeFieldID=0; +jfieldID KernelArg::isStaticFieldID=0; +jfieldID KernelArg::javaArrayFieldID=0; +jfieldID KernelArg::sizeInBytesFieldID=0; +jfieldID KernelArg::numElementsFieldID=0; class JNIContext{ private: @@ -365,10 +483,7 @@ class JNIContext{ cl_platform_id* platforms; cl_uint platformc; public: - JNIEnv *jenv; jobject kernelObject; - jint numProcessors; - jint maxJTPLocalSize; jclass kernelClass; cl_uint deviceIdc; cl_device_id* deviceIds; @@ -391,6 +506,7 @@ class JNIContext{ // these map to camelCase form of CL_DEVICE_XXX_XXX For example CL_DEVICE_MAX_COMPUTE_UNITS == maxComputeUnits cl_uint maxComputeUnits; cl_uint maxWorkItemDimensions; + size_t *maxWorkItemSizes; size_t maxWorkGroupSize; cl_ulong globalMemSize; cl_ulong localMemSize; @@ -399,13 +515,10 @@ class JNIContext{ return((JNIContext*)jniContextHandle); } - JNIContext(JNIEnv *_jenv, jobject _kernelObject, jint _flags, jint _numProcessors, jint _maxJTPLocalSize): - jenv(_jenv), + JNIContext(JNIEnv *jenv, jobject _kernelObject, jint _flags): kernelObject(jenv->NewGlobalRef(_kernelObject)), kernelClass((jclass)jenv->NewGlobalRef(jenv->GetObjectClass(_kernelObject))), flags(_flags), - numProcessors(_numProcessors), - maxJTPLocalSize(_maxJTPLocalSize), platform(NULL), profileBaseTime(0), deviceType(((flags&com_amd_aparapi_KernelRunner_JNI_FLAG_USE_GPU)==com_amd_aparapi_KernelRunner_JNI_FLAG_USE_GPU)?CL_DEVICE_TYPE_GPU:CL_DEVICE_TYPE_CPU), @@ -438,1179 +551,1209 @@ class JNIContext{ // platformVersionName = "OpenCL 1.1 AMD-APP-SDK-v2.5 (684.213)"|"OpenCL 1.1 CUDA 4.0.1" #ifndef __APPLE__ // Here we check if the platformVersionName starts with "OpenCL 1.1" (10 chars!) - if (!strncmp(platformVersionName, "OpenCL 1.1", 10)) { + if (!strncmp(platformVersionName, "OpenCL 1.1", 10)) { //} #else - // Here we check if the platformVersionName starts with "OpenCL 1.1" or "OpenCL 1.0" (10 chars!) - if (!strncmp(platformVersionName, "OpenCL 1.1", 10) || !strncmp(platformVersionName, "OpenCL 1.0", 10)) { + // Here we check if the platformVersionName starts with "OpenCL 1.1" or "OpenCL 1.0" (10 chars!) + if (!strncmp(platformVersionName, "OpenCL 1.1", 10) || !strncmp(platformVersionName, "OpenCL 1.0", 10)) { // } #endif - // Get the # of devices - status = clGetDeviceIDs(platforms[i], deviceType, 0, NULL, &deviceIdc); - // now check if this platform supports the requested device type (GPU or CPU) - if (status == CL_SUCCESS && deviceIdc >0 ){ - if (deviceIdc >1){ - if (isVerbose()){ - fprintf(stderr, "Warning attempt to use %d devices\n", deviceIdc); - } - deviceIdc = 1; // Hack to work around issue #18 (multiple device error) - if (isVerbose()){ - fprintf(stderr, "Locking deviceIdc to %d to work around issue #18\n", deviceIdc); - } - } - platform = platforms[i]; - if (isVerbose()){ - fprintf(stderr, "platform %s supports requested device type\n", platformVendorName); - } - - deviceIds = new cl_device_id[deviceIdc]; - status = clGetDeviceIDs(platform, deviceType, deviceIdc, deviceIds, NULL); - if (status == CL_SUCCESS){ - ASSERT_CL_NO_RETURN("clGetDeviceIDs()"); - - GET_DEV_INFO(deviceIds[0], CL_DEVICE_MAX_COMPUTE_UNITS, maxComputeUnits, "%d"); - GET_DEV_INFO(deviceIds[0], CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS, maxWorkItemDimensions, "%d"); - GET_DEV_INFO(deviceIds[0], CL_DEVICE_MAX_WORK_GROUP_SIZE, maxWorkGroupSize, "%d"); - GET_DEV_INFO(deviceIds[0], CL_DEVICE_GLOBAL_MEM_SIZE, globalMemSize, "%d"); - GET_DEV_INFO(deviceIds[0], CL_DEVICE_LOCAL_MEM_SIZE, localMemSize, "%d"); - if (isVerbose()){ - - fprintf(stderr, "device[%p]: Type: ", deviceIds[0]); - if (deviceType & CL_DEVICE_TYPE_DEFAULT) { - // deviceType &= ~CL_DEVICE_TYPE_DEFAULT; - fprintf(stderr, "Default "); - }else if (deviceType & CL_DEVICE_TYPE_CPU) { - // deviceType &= ~CL_DEVICE_TYPE_CPU; - fprintf(stderr, "CPU "); - }else if (deviceType & CL_DEVICE_TYPE_GPU) { - // deviceType &= ~CL_DEVICE_TYPE_GPU; - fprintf(stderr, "GPU "); - }else if (deviceType & CL_DEVICE_TYPE_ACCELERATOR) { - // deviceType &= ~CL_DEVICE_TYPE_ACCELERATOR; - fprintf(stderr, "Accelerator "); - }else{ - fprintf(stderr, "Unknown (0x%llx) ", deviceType); - } - fprintf(stderr, "\n"); - } - cl_context_properties cps[3] = { CL_CONTEXT_PLATFORM, (cl_context_properties)platform, 0 }; - cl_context_properties* cprops = (NULL == platform) ? NULL : cps; - context = clCreateContextFromType( cprops, deviceType, NULL, NULL, &status); - ASSERT_CL_NO_RETURN("clCreateContextFromType()"); - if (status == CL_SUCCESS){ - - valid = JNI_TRUE; - } - } - }else{ - if (isVerbose()){ - fprintf(stderr, "platform %s does not support requested device type skipping!\n", platformVendorName); - } + // Get the # of devices + status = clGetDeviceIDs(platforms[i], deviceType, 0, NULL, &deviceIdc); + // now check if this platform supports the requested device type (GPU or CPU) + if (status == CL_SUCCESS && deviceIdc >0 ){ + if (deviceIdc >1){ + if (isVerbose()){ + fprintf(stderr, "Warning attempt to use %d devices\n", deviceIdc); } - - }else{ + deviceIdc = 1; // Hack to work around issue #18 (multiple device error) if (isVerbose()){ -#ifndef __APPLE__ - fprintf(stderr, "platform %s version %s is not OpenCL 1.1 skipping!\n", platformVendorName, platformVersionName); -#else - fprintf(stderr, "platform %s version %s is neither OpenCL 1.1 or OpenCL 1.0 skipping!\n", platformVendorName, platformVersionName); -#endif - + fprintf(stderr, "Locking deviceIdc to %d to work around issue #18\n", deviceIdc); } } - } + platform = platforms[i]; + if (isVerbose()){ + fprintf(stderr, "platform %s supports requested device type\n", platformVendorName); + } - } - }else{ - if (isVerbose()){ - fprintf(stderr, "no opencl platforms available!\n"); - } - } + deviceIds = new cl_device_id[deviceIdc]; + status = clGetDeviceIDs(platform, deviceType, deviceIdc, deviceIds, NULL); + ASSERT_CL_NO_RETURN("clGetDeviceIDs()"); + if (status == CL_SUCCESS){ - } + status = clGetDeviceInfo(deviceIds[0], CL_DEVICE_MAX_COMPUTE_UNITS, sizeof(maxComputeUnits), &maxComputeUnits, NULL); + ASSERT_CL_NO_RETURN( "clGetDeviceInfo(CL_DEVICE_MAX_COMPUTE_UNITS)."); - jboolean isValid(){ - return(valid); - } - jboolean isProfilingEnabled(){ - return((flags&com_amd_aparapi_KernelRunner_JNI_FLAG_ENABLE_PROFILING)==com_amd_aparapi_KernelRunner_JNI_FLAG_ENABLE_PROFILING?JNI_TRUE:JNI_FALSE); - } - jboolean isUsingGPU(){ - return((flags&com_amd_aparapi_KernelRunner_JNI_FLAG_USE_GPU)==com_amd_aparapi_KernelRunner_JNI_FLAG_USE_GPU?JNI_TRUE:JNI_FALSE); - } - jboolean isVerbose(){ - return((flags&com_amd_aparapi_KernelRunner_JNI_FLAG_ENABLE_VERBOSE_JNI)==com_amd_aparapi_KernelRunner_JNI_FLAG_ENABLE_VERBOSE_JNI?JNI_TRUE:JNI_FALSE); - } + status = clGetDeviceInfo(deviceIds[0], CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS, sizeof(maxWorkItemDimensions), &maxWorkItemDimensions, NULL); + ASSERT_CL_NO_RETURN( "clGetDeviceInfo(CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS)."); - ~JNIContext(){ - cl_int status = CL_SUCCESS; - jenv->DeleteGlobalRef(kernelObject); - jenv->DeleteGlobalRef(kernelClass); - if (context != 0){ - status = clReleaseContext(context); - ASSERT_CL_NO_RETURN("clReleaseContext()"); - context = (cl_context)0; - } - if (commandQueues){ - for (int dev=0; dev<deviceIdc; dev++){ - status = clReleaseCommandQueue((cl_command_queue)commandQueues[dev]); - ASSERT_CL_NO_RETURN("clReleaseCommandQueue()"); - commandQueues[dev] = (cl_command_queue)0; - } - delete[] commandQueues; commandQueues = NULL; - } - if (program != 0){ - status = clReleaseProgram((cl_program)program); - ASSERT_CL_NO_RETURN("clReleaseProgram()"); - program = (cl_program)0; - } - if (kernel != 0){ - status = clReleaseKernel((cl_kernel)kernel); - ASSERT_CL_NO_RETURN("clReleaseKernel()"); - kernel = (cl_kernel)0; - } - if (platforms){ - delete []platforms; platforms=NULL; - } - if (deviceIds){ - delete [] deviceIds; deviceIds=NULL; - } - if (argc> 0){ - for (int i=0; i< argc; i++){ - KernelArg *arg = args[i]; - if (!arg->isPrimitive()){ - if (arg->value.ref.mem != 0){ - status = clReleaseMemObject((cl_mem)arg->value.ref.mem); - ASSERT_CL_NO_RETURN("clReleaseMemObject()"); - arg->value.ref.mem = (cl_mem)0; + maxWorkItemSizes = (size_t *)malloc(sizeof(size_t)*maxWorkItemDimensions); + status = clGetDeviceInfo(deviceIds[0], CL_DEVICE_MAX_WORK_ITEM_SIZES, sizeof(size_t)*maxWorkItemDimensions, maxWorkItemSizes, NULL); + + ASSERT_CL_NO_RETURN( "clGetDeviceInfo(CL_DEVICE_MAX_WORK_ITEM_SIZES)."); + + status = clGetDeviceInfo(deviceIds[0], CL_DEVICE_MAX_WORK_GROUP_SIZE, sizeof(maxWorkGroupSize), &maxWorkGroupSize, NULL); + ASSERT_CL_NO_RETURN( "clGetDeviceInfo(CL_DEVICE_MAX_WORK_GROUP_SIZE)."); + + status = clGetDeviceInfo(deviceIds[0], CL_DEVICE_GLOBAL_MEM_SIZE, sizeof(globalMemSize), &globalMemSize, NULL); + ASSERT_CL_NO_RETURN( "clGetDeviceInfo(CL_DEVICE_GLOBAL_MEM_SIZE)."); + + status = clGetDeviceInfo(deviceIds[0], CL_DEVICE_LOCAL_MEM_SIZE, sizeof(localMemSize), &localMemSize, NULL); + + ASSERT_CL_NO_RETURN( "clGetDeviceInfo(CL_DEVICE_LOCAL_MEM_SIZE)."); + + + if (isVerbose()){ + fprintf(stderr, "device[%p]: Type: ", deviceIds[0]); + if (deviceType & CL_DEVICE_TYPE_DEFAULT) { + // deviceType &= ~CL_DEVICE_TYPE_DEFAULT; + fprintf(stderr, "Default "); + }else if (deviceType & CL_DEVICE_TYPE_CPU) { + // deviceType &= ~CL_DEVICE_TYPE_CPU; + fprintf(stderr, "CPU "); + }else if (deviceType & CL_DEVICE_TYPE_GPU) { + // deviceType &= ~CL_DEVICE_TYPE_GPU; + fprintf(stderr, "GPU "); + }else if (deviceType & CL_DEVICE_TYPE_ACCELERATOR) { + // deviceType &= ~CL_DEVICE_TYPE_ACCELERATOR; + fprintf(stderr, "Accelerator "); + }else{ + fprintf(stderr, "Unknown (0x%llx) ", deviceType); + } + fprintf(stderr, "\n"); + } + cl_context_properties cps[3] = { CL_CONTEXT_PLATFORM, (cl_context_properties)platform, 0 }; + cl_context_properties* cprops = (NULL == platform) ? NULL : cps; + context = clCreateContextFromType( cprops, deviceType, NULL, NULL, &status); + ASSERT_CL_NO_RETURN("clCreateContextFromType()"); + if (status == CL_SUCCESS){ + + valid = JNI_TRUE; + } } - if (arg->value.ref.javaArray != NULL) { - jenv->DeleteWeakGlobalRef((jweak) arg->value.ref.javaArray); + }else{ + if (isVerbose()){ + fprintf(stderr, "platform %s does not support requested device type skipping!\n", platformVendorName); } } - if (arg->name != NULL){ - free(arg->name); arg->name = NULL; - } - if (arg->javaArg != NULL ) { - jenv->DeleteGlobalRef((jobject) arg->javaArg); - } - delete arg; arg=args[i]=NULL; - } - delete[] args; args=NULL; - delete []readEvents; readEvents =NULL; - delete []writeEvents; writeEvents = NULL; - delete []executeEvents; executeEvents = NULL; + }else{ + if (isVerbose()){ +#ifndef __APPLE__ + fprintf(stderr, "platform %s version %s is not OpenCL 1.1 skipping!\n", platformVendorName, platformVersionName); +#else + fprintf(stderr, "platform %s version %s is neither OpenCL 1.1 or OpenCL 1.0 skipping!\n", platformVendorName, platformVersionName); +#endif - if (isProfilingEnabled()) { - if (profileFile != NULL && profileFile != stderr) { - fclose(profileFile); } - delete[] readEventArgs; readEventArgs=0; - delete[] writeEventArgs; writeEventArgs=0; - } - } - } - - /* - Release JNI critical pinned arrays before returning to java code - */ - void unpinAll() { - for (int i=0; i< argc; i++){ - KernelArg *arg = args[i]; - if (arg->isBackedByArray()) { - arg->unpin(jenv); } } + + } + }else{ + if (isVerbose()){ + fprintf(stderr, "no opencl platforms available!\n"); } + } +} - }; +jboolean isValid(){ + return(valid); +} +jboolean isProfilingEnabled(){ + return((flags&com_amd_aparapi_KernelRunner_JNI_FLAG_ENABLE_PROFILING)==com_amd_aparapi_KernelRunner_JNI_FLAG_ENABLE_PROFILING?JNI_TRUE:JNI_FALSE); +} +jboolean isUsingGPU(){ + return((flags&com_amd_aparapi_KernelRunner_JNI_FLAG_USE_GPU)==com_amd_aparapi_KernelRunner_JNI_FLAG_USE_GPU?JNI_TRUE:JNI_FALSE); +} +jboolean isVerbose(){ + return((flags&com_amd_aparapi_KernelRunner_JNI_FLAG_ENABLE_VERBOSE_JNI)==com_amd_aparapi_KernelRunner_JNI_FLAG_ENABLE_VERBOSE_JNI?JNI_TRUE:JNI_FALSE); +} - jclass cacheKernelArgFields(JNIEnv *jenv, jobject jobj){ - jclass c = jenv->GetObjectClass(jobj); - nameFieldID = jenv->GetFieldID(c, "name", "Ljava/lang/String;"); ASSERT_FIELD(name); - typeFieldID = jenv->GetFieldID(c, "type", "I"); ASSERT_FIELD(type); - isStaticFieldID = jenv->GetFieldID(c, "isStatic", "Z"); ASSERT_FIELD(isStatic); - javaArrayFieldID = jenv->GetFieldID(c, "javaArray", "Ljava/lang/Object;"); ASSERT_FIELD(javaArray); - bytesPerLocalSizeFieldID = jenv->GetFieldID(c, "bytesPerLocalSize", "I"); ASSERT_FIELD(bytesPerLocalSize); - sizeInBytesFieldID = jenv->GetFieldID(c, "sizeInBytes", "I"); ASSERT_FIELD(sizeInBytes); - numElementsFieldID = jenv->GetFieldID(c, "numElements", "I"); ASSERT_FIELD(numElements); - return(c); - } +~JNIContext(){ +} - JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_disposeJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle) { - cl_int status = CL_SUCCESS; - JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); - if (jniContext != NULL){ - delete jniContext;//free(jniContext); - jniContext = NULL; - } - return(status); +void dispose(JNIEnv *jenv){ + cl_int status = CL_SUCCESS; + jenv->DeleteGlobalRef(kernelObject); + jenv->DeleteGlobalRef(kernelClass); + if (context != 0){ + status = clReleaseContext(context); + ASSERT_CL_NO_RETURN("clReleaseContext()"); + context = (cl_context)0; + } + if (commandQueues){ + for (int dev=0; dev<deviceIdc; dev++){ + status = clReleaseCommandQueue((cl_command_queue)commandQueues[dev]); + ASSERT_CL_NO_RETURN("clReleaseCommandQueue()"); + commandQueues[dev] = (cl_command_queue)0; } - - void idump(char *str, void *ptr, int size){ - int * iptr = (int *)ptr; - for (int i=0; i<size/sizeof(int); i++){ - fprintf(stderr, "%s%4d %d\n", str, i, iptr[i]); + delete[] commandQueues; commandQueues = NULL; + } + if (program != 0){ + status = clReleaseProgram((cl_program)program); + ASSERT_CL_NO_RETURN("clReleaseProgram()"); + program = (cl_program)0; + } + if (kernel != 0){ + status = clReleaseKernel((cl_kernel)kernel); + ASSERT_CL_NO_RETURN("clReleaseKernel()"); + kernel = (cl_kernel)0; + } + if (platforms){ + delete []platforms; platforms=NULL; + } + if (deviceIds){ + delete [] deviceIds; deviceIds=NULL; + } + if (argc> 0){ + for (int i=0; i< argc; i++){ + KernelArg *arg = args[i]; + if (!arg->isPrimitive()){ + if (arg->value.ref.mem != 0){ + status = clReleaseMemObject((cl_mem)arg->value.ref.mem); + ASSERT_CL_NO_RETURN("clReleaseMemObject()"); + arg->value.ref.mem = (cl_mem)0; + } + if (arg->value.ref.javaArray != NULL) { + jenv->DeleteWeakGlobalRef((jweak) arg->value.ref.javaArray); + } + } + if (arg->name != NULL){ + free(arg->name); arg->name = NULL; } + if (arg->javaArg != NULL ) { + jenv->DeleteGlobalRef((jobject) arg->javaArg); + } + delete arg; arg=args[i]=NULL; } + delete[] args; args=NULL; + + delete []readEvents; readEvents =NULL; + delete []writeEvents; writeEvents = NULL; + delete []executeEvents; executeEvents = NULL; - void fdump(char *str, void *ptr, int size){ - float * fptr = (float *)ptr; - for (int i=0; i<size/sizeof(float); i++){ - fprintf(stderr, "%s%4d %6.2f\n", str, i, fptr[i]); + if (isProfilingEnabled()) { + if (profileFile != NULL && profileFile != stderr) { + fclose(profileFile); } + delete[] readEventArgs; readEventArgs=0; + delete[] writeEventArgs; writeEventArgs=0; + } + } +} + +/* + Release JNI critical pinned arrays before returning to java code + */ +void unpinAll(JNIEnv* jenv) { + for (int i=0; i< argc; i++){ + KernelArg *arg = args[i]; + if (arg->isBackedByArray()) { + arg->unpin(jenv); } + } +} - jint writeProfileInfo(JNIContext* jniContext){ - cl_ulong currSampleBaseTime = -1; - int pos = 1; +}; - if (jniContext->firstRun) { - fprintf(jniContext->profileFile, "# PROFILE Name, queued, submit, start, end (microseconds)\n"); - } - // A read by a user kernel means the OpenCL layer wrote to the kernel and vice versa - for (int i=0; i< jniContext->argc; i++){ - KernelArg *arg=jniContext->args[i]; - if (arg->isBackedByArray() && arg->isRead()){ - // Initialize the base time for this sample - if (currSampleBaseTime == -1) { - currSampleBaseTime = arg->value.ref.write.queued; - } - if (jniContext->profileBaseTime == 0){ - jniContext->profileBaseTime = arg->value.ref.write.queued; +JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_disposeJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle) { + cl_int status = CL_SUCCESS; + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + if (jniContext != NULL){ + jniContext->dispose(jenv); + delete jniContext; + jniContext = NULL; + } + return(status); +} - // Write the base time as the first item in the csv - //fprintf(jniContext->profileFile, "%llu,", jniContext->profileBaseTime); - } +void idump(char *str, void *ptr, int size){ + int * iptr = (int *)ptr; + for (int i=0; i<size/sizeof(int); i++){ + fprintf(stderr, "%s%4d %d\n", str, i, iptr[i]); + } +} - fprintf(jniContext->profileFile, "%d write %s,", pos++, arg->name); +void fdump(char *str, void *ptr, int size){ + float * fptr = (float *)ptr; + for (int i=0; i<size/sizeof(float); i++){ + fprintf(stderr, "%s%4d %6.2f\n", str, i, fptr[i]); + } +} - fprintf(jniContext->profileFile, "%lu,%lu,%lu,%lu,", - (arg->value.ref.write.queued - currSampleBaseTime)/1000, - (arg->value.ref.write.submit - currSampleBaseTime)/1000, - (arg->value.ref.write.start - currSampleBaseTime)/1000, - (arg->value.ref.write.end - currSampleBaseTime)/1000); - } - } - if (jniContext->profileBaseTime == 0){ - jniContext->profileBaseTime = jniContext->exec.queued; +jint writeProfileInfo(JNIContext* jniContext){ + cl_ulong currSampleBaseTime = -1; + int pos = 1; - // Write the base time as the first item in the csv - //fprintf(jniContext->profileFile, "%llu,", jniContext->profileBaseTime); - } + if (jniContext->firstRun) { + fprintf(jniContext->profileFile, "# PROFILE Name, queued, submit, start, end (microseconds)\n"); + } - // Initialize the base time for this sample if necessary + // A read by a user kernel means the OpenCL layer wrote to the kernel and vice versa + for (int i=0; i< jniContext->argc; i++){ + KernelArg *arg=jniContext->args[i]; + if (arg->isBackedByArray() && arg->isRead()){ + + // Initialize the base time for this sample if (currSampleBaseTime == -1) { - currSampleBaseTime = jniContext->exec.queued; + currSampleBaseTime = arg->value.ref.write.queued; } - // exec - fprintf(jniContext->profileFile, "%d exec,", pos++); - - fprintf(jniContext->profileFile, "%lu,%lu,%lu,%lu,", - (jniContext->exec.queued - currSampleBaseTime)/1000, - (jniContext->exec.submit - currSampleBaseTime)/1000, - (jniContext->exec.start - currSampleBaseTime)/1000, - (jniContext->exec.end - currSampleBaseTime)/1000); - - // - if ( jniContext->argc == 0 ) { - fprintf(jniContext->profileFile, "\n"); - } else { - for (int i=0; i< jniContext->argc; i++){ - KernelArg *arg=jniContext->args[i]; - if (arg->isBackedByArray() && arg->isWrite()){ - if (jniContext->profileBaseTime == 0){ - jniContext->profileBaseTime = arg->value.ref.read.queued; - - // Write the base time as the first item in the csv - //fprintf(jniContext->profileFile, "%llu,", jniContext->profileBaseTime); - } - - // Initialize the base time for this sample - if (currSampleBaseTime == -1) { - currSampleBaseTime = arg->value.ref.read.queued; - } - - fprintf(jniContext->profileFile, "%d read %s,", pos++, arg->name); + if (jniContext->profileBaseTime == 0){ + jniContext->profileBaseTime = arg->value.ref.write.queued; - fprintf(jniContext->profileFile, "%lu,%lu,%lu,%lu,", - (arg->value.ref.read.queued - currSampleBaseTime)/1000, - (arg->value.ref.read.submit - currSampleBaseTime)/1000, - (arg->value.ref.read.start - currSampleBaseTime)/1000, - (arg->value.ref.read.end - currSampleBaseTime)/1000); - } - } + // Write the base time as the first item in the csv + //fprintf(jniContext->profileFile, "%llu,", jniContext->profileBaseTime); } - fprintf(jniContext->profileFile, "\n"); - return(0); - } - - // Should failed profiling abort the run and return early? - cl_int profile(ProfileInfo *profileInfo, cl_event *event){ - cl_int status = CL_SUCCESS; - status = clGetEventProfilingInfo(*event, CL_PROFILING_COMMAND_QUEUED, sizeof(profileInfo->queued), &(profileInfo->queued), NULL); - ASSERT_CL( "clGetEventProfiliningInfo() QUEUED"); - status = clGetEventProfilingInfo(*event, CL_PROFILING_COMMAND_SUBMIT, sizeof(profileInfo->submit), &(profileInfo->submit), NULL); - ASSERT_CL( "clGetEventProfiliningInfo() SUBMIT"); - status = clGetEventProfilingInfo(*event, CL_PROFILING_COMMAND_START, sizeof(profileInfo->start), &(profileInfo->start), NULL); - ASSERT_CL( "clGetEventProfiliningInfo() START"); - status = clGetEventProfilingInfo(*event, CL_PROFILING_COMMAND_END, sizeof(profileInfo->end), &(profileInfo->end), NULL); - ASSERT_CL( "clGetEventProfiliningInfo() END"); - return status; - } - + fprintf(jniContext->profileFile, "%d write %s,", pos++, arg->name); + fprintf(jniContext->profileFile, "%lu,%lu,%lu,%lu,", + (arg->value.ref.write.queued - currSampleBaseTime)/1000, + (arg->value.ref.write.submit - currSampleBaseTime)/1000, + (arg->value.ref.write.start - currSampleBaseTime)/1000, + (arg->value.ref.write.end - currSampleBaseTime)/1000); + } + } - jint updateKernel(JNIEnv *jenv, jobject jobj, JNIContext* jniContext) { - cl_int status = CL_SUCCESS; - if (jniContext != NULL){ - // we need to step through the array of KernelArg's to create the info required to create the cl_mem buffers. - for (jint i=0; i<jniContext->argc; i++){ - KernelArg *arg=jniContext->args[i]; - - arg->type = jenv->GetIntField(arg->javaArg, typeFieldID); - if (jniContext->isVerbose()){ - fprintf(stderr, "got type for %s: %08x\n", arg->name, arg->type); - } - if (!arg->isPrimitive()) { - // Following used for all primitive arrays, object arrays and nio Buffers - jarray newRef = (jarray)jenv->GetObjectField(arg->javaArg, javaArrayFieldID); - if (jniContext->isVerbose()){ - - fprintf(stderr, "testing for Resync javaArray %s: old=%p, new=%p\n", arg->name, arg->value.ref.javaArray, newRef); - } - - jboolean isSame = jenv->IsSameObject( newRef, arg->value.ref.javaArray); - if (isSame == JNI_FALSE) { - if (jniContext->isVerbose()){ - fprintf(stderr, "Resync javaArray for %s: %p %p\n", arg->name, newRef, arg->value.ref.javaArray); - } - // Free previous ref if any - if (arg->value.ref.javaArray != NULL) { - jenv->DeleteWeakGlobalRef((jweak) arg->value.ref.javaArray); - if (jniContext->isVerbose()){ - fprintf(stderr, "DeleteWeakGlobalRef for %s: %p\n", arg->name, arg->value.ref.javaArray); - } - } - - // need to free opencl buffers, run will reallocate later - if (arg->value.ref.mem != 0) { - //fprintf(stderr, "-->releaseMemObject[%d]\n", i); - status = clReleaseMemObject((cl_mem)arg->value.ref.mem); - //fprintf(stderr, "<--releaseMemObject[%d]\n", i); - ASSERT_CL("clReleaseMemObject()"); - arg->value.ref.mem = (cl_mem)0; - } - - arg->value.ref.mem = (cl_mem) 0; - arg->value.ref.addr = NULL; + if (jniContext->profileBaseTime == 0){ + jniContext->profileBaseTime = jniContext->exec.queued; - // Capture new array ref from the kernel arg object + // Write the base time as the first item in the csv + //fprintf(jniContext->profileFile, "%llu,", jniContext->profileBaseTime); + } - if (newRef != NULL) { - arg->value.ref.javaArray = (jarray)jenv->NewWeakGlobalRef((jarray)newRef); - if (jniContext->isVerbose()){ - fprintf(stderr, "NewWeakGlobalRef for %s, set to %p\n", arg->name, - arg->value.ref.javaArray); - } - } else { - arg->value.ref.javaArray = NULL; - } - arg->value.ref.isArray = !arg->isAparapiBufIsDirect(); + // Initialize the base time for this sample if necessary + if (currSampleBaseTime == -1) { + currSampleBaseTime = jniContext->exec.queued; + } + + // exec + fprintf(jniContext->profileFile, "%d exec,", pos++); + + fprintf(jniContext->profileFile, "%lu,%lu,%lu,%lu,", + (jniContext->exec.queued - currSampleBaseTime)/1000, + (jniContext->exec.submit - currSampleBaseTime)/1000, + (jniContext->exec.start - currSampleBaseTime)/1000, + (jniContext->exec.end - currSampleBaseTime)/1000); + + // + if ( jniContext->argc == 0 ) { + fprintf(jniContext->profileFile, "\n"); + } else { + for (int i=0; i< jniContext->argc; i++){ + KernelArg *arg=jniContext->args[i]; + if (arg->isBackedByArray() && arg->isWrite()){ + if (jniContext->profileBaseTime == 0){ + jniContext->profileBaseTime = arg->value.ref.read.queued; + + // Write the base time as the first item in the csv + //fprintf(jniContext->profileFile, "%llu,", jniContext->profileBaseTime); + } - // Save the sizeInBytes which was set on the java side - arg->sizeInBytes = jenv->GetIntField(arg->javaArg, sizeInBytesFieldID); + // Initialize the base time for this sample + if (currSampleBaseTime == -1) { + currSampleBaseTime = arg->value.ref.read.queued; + } - if (jniContext->isVerbose()){ - fprintf(stderr, "updateKernel, args[%d].sizeInBytes=%d\n", i, arg->sizeInBytes); - } - } // !is_same - } - } // for each arg - } // if jniContext != NULL + fprintf(jniContext->profileFile, "%d read %s,", pos++, arg->name); - return(status); + fprintf(jniContext->profileFile, "%lu,%lu,%lu,%lu,", + (arg->value.ref.read.queued - currSampleBaseTime)/1000, + (arg->value.ref.read.submit - currSampleBaseTime)/1000, + (arg->value.ref.read.start - currSampleBaseTime)/1000, + (arg->value.ref.read.end - currSampleBaseTime)/1000); + } } + } + fprintf(jniContext->profileFile, "\n"); + return(0); +} +// Should failed profiling abort the run and return early? +cl_int profile(ProfileInfo *profileInfo, cl_event *event){ + cl_int status = CL_SUCCESS; + status = clGetEventProfilingInfo(*event, CL_PROFILING_COMMAND_QUEUED, sizeof(profileInfo->queued), &(profileInfo->queued), NULL); + ASSERT_CL( "clGetEventProfiliningInfo() QUEUED"); + status = clGetEventProfilingInfo(*event, CL_PROFILING_COMMAND_SUBMIT, sizeof(profileInfo->submit), &(profileInfo->submit), NULL); + ASSERT_CL( "clGetEventProfiliningInfo() SUBMIT"); + status = clGetEventProfilingInfo(*event, CL_PROFILING_COMMAND_START, sizeof(profileInfo->start), &(profileInfo->start), NULL); + ASSERT_CL( "clGetEventProfiliningInfo() START"); + status = clGetEventProfilingInfo(*event, CL_PROFILING_COMMAND_END, sizeof(profileInfo->end), &(profileInfo->end), NULL); + ASSERT_CL( "clGetEventProfiliningInfo() END"); + return status; +} - JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_runKernelJNI(JNIEnv *jenv, - jobject jobj, jlong jniContextHandle, jint globalSize, jint localSize, jboolean needSync, - jboolean useNullForLocalSize, jint passes) { +//Step through all non-primitive (array of primitive or array object references) and determine if the field has changed +//The field may have been re-assigned by the Java code to NULL or another instance. +//If we detect a change then we discard the previous cl_mem buffer, the caller will detect that the buffers are null and will create new cl_mem buffers. +jint updateNonPrimitiveReferences(JNIEnv *jenv, jobject jobj, JNIContext* jniContext) { + cl_int status = CL_SUCCESS; + if (jniContext != NULL){ + for (jint i=0; i<jniContext->argc; i++){ + KernelArg *arg=jniContext->args[i]; + arg->syncType(jenv); // make sure that the JNI arg reflects the latest type info from the instance - cl_int status = CL_SUCCESS; - JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); if (jniContext->isVerbose()){ - timer.start(); + fprintf(stderr, "got type for %s: %08x\n", arg->name, arg->type); } - - // Need to capture array refs - if (jniContext->firstRun || needSync) { - updateKernel(jenv, jobj, jniContext ); + if (!arg->isPrimitive()) { + // Following used for all primitive arrays, object arrays and nio Buffers + jarray newRef = (jarray)jenv->GetObjectField(arg->javaArg, KernelArg::javaArrayFieldID); if (jniContext->isVerbose()){ - fprintf(stderr, "back from updateKernel\n"); + fprintf(stderr, "testing for Resync javaArray %s: old=%p, new=%p\n", arg->name, arg->value.ref.javaArray, newRef); } - } - - int writeEventCount = 0; - - // kernelArgPos is used to keep track of the kernel arg position, it can - // differ from "i" due to insertion of javaArrayLength args which are not - // fields read from the kernel object. - int kernelArgPos = 0; - - for (int i=0; i< jniContext->argc; i++){ - KernelArg *arg = jniContext->args[i]; - // TODO: see if we can get rid of this read - arg->type = jenv->GetIntField(arg->javaArg, typeFieldID); - if (jniContext->isVerbose()){ - fprintf(stderr, "got type for arg %d, %s, type=%08x\n", i, arg->name, arg->type); - } - if (!arg->isPrimitive() && !arg->isLocal()) { - // pin the arrays so that GC does not move them during the call - - // get the C memory address for the region being transferred - // this uses different JNI calls for arrays vs. directBufs - void * prevAddr = arg->value.ref.addr; - if (arg->value.ref.isArray) { - arg->pin(jenv); - } else if (arg->isAparapiBufIsDirect()) { - // different call used for directbuffers - arg->value.ref.addr = jenv->GetDirectBufferAddress(arg->value.ref.javaArray); - } + if (!jenv->IsSameObject(newRef, arg->value.ref.javaArray)) { if (jniContext->isVerbose()){ - fprintf(stderr, "runKernel: arrayOrBuf ref %p, oldAddr=%p, newAddr=%p, ref.mem=%p, isArray=%d\n", - arg->value.ref.javaArray, - prevAddr, - arg->value.ref.addr, - arg->value.ref.mem, - arg->value.ref.isArray ); - fprintf(stderr, "at memory addr %p, contents: ", arg->value.ref.addr); - unsigned char *pb = (unsigned char *) arg->value.ref.addr; - for (int k=0; k<8; k++) { - fprintf(stderr, "%02x ", pb[k]); - } - fprintf(stderr, "\n" ); + fprintf(stderr, "Resync javaArray for %s: %p %p\n", arg->name, newRef, arg->value.ref.javaArray); } - // record whether object moved - // if we see that isCopy was returned by getPrimitiveArrayCritical, treat that as a move - bool objectMoved = (arg->value.ref.addr != prevAddr) || arg->value.ref.isCopy; - -#ifdef VERBOSE_EXPLICIT - if (arg->isExplicit() && arg->isExplicitWrite()){ - fprintf(stderr, "explicit write of %s\n", arg->name); - } -#endif - - if (jniContext->firstRun || (arg->value.ref.mem == 0) || objectMoved ){ - // if either this is the first run or user changed input array - // or gc moved something, then we create buffers/args - cl_uint mask = CL_MEM_USE_HOST_PTR; - if (arg->isRead() && arg->isWrite()) mask |= CL_MEM_READ_WRITE; - else if (arg->isRead() && !arg->isWrite()) mask |= CL_MEM_READ_ONLY; - else if (arg->isWrite()) mask |= CL_MEM_WRITE_ONLY; - arg->value.ref.memMask = mask; + // Free previous ref if any + if (arg->value.ref.javaArray != NULL) { + jenv->DeleteWeakGlobalRef((jweak) arg->value.ref.javaArray); if (jniContext->isVerbose()){ - strcpy(arg->value.ref.memSpec,"CL_MEM_USE_HOST_PTR"); - if (mask & CL_MEM_READ_WRITE) strcat(arg->value.ref.memSpec,"|CL_MEM_READ_WRITE"); - if (mask & CL_MEM_READ_ONLY) strcat(arg->value.ref.memSpec,"|CL_MEM_READ_ONLY"); - if (mask & CL_MEM_WRITE_ONLY) strcat(arg->value.ref.memSpec,"|CL_MEM_WRITE_ONLY"); - - fprintf(stderr, "%s %d clCreateBuffer(context, %s, size=%08x bytes, address=%08x, &status)\n", arg->name, - i, arg->value.ref.memSpec, arg->sizeInBytes, arg->value.ref.addr); - } - arg->value.ref.mem = clCreateBuffer(jniContext->context, arg->value.ref.memMask, - arg->sizeInBytes, arg->value.ref.addr, &status); - - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clCreateBuffer"); - jniContext->unpinAll(); - return status; - } - - status = clSetKernelArg(jniContext->kernel, kernelArgPos++, sizeof(cl_mem), (void *)&(arg->value.ref.mem)); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clSetKernelArg (array)"); - jniContext->unpinAll(); - return status; - } - - // Add the array length if needed - if (arg->usesArrayLength()){ - arg->value.ref.javaArrayLength = jenv->GetIntField(arg->javaArg, numElementsFieldID); - status = clSetKernelArg(jniContext->kernel, kernelArgPos++, sizeof(jint), &(arg->value.ref.javaArrayLength)); - - if (jniContext->isVerbose()){ - fprintf(stderr, "runKernel arg %d %s, javaArrayLength = %d\n", i, arg->name, arg->value.ref.javaArrayLength); - } - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clSetKernelArg (array length)"); - jniContext->unpinAll(); - return status; - } - } - } else { - // Keep the arg position in sync if no updates were required - kernelArgPos++; - if (arg->usesArrayLength()){ - kernelArgPos++; + fprintf(stderr, "DeleteWeakGlobalRef for %s: %p\n", arg->name, arg->value.ref.javaArray); } } - // we only enqueue a write if we know the kernel actually reads the buffer or if there is an explicit write pending - // the default behavior for Constant buffers is also that there is no write enqueued unless explicit + // need to free opencl buffers, run will reallocate later + if (arg->value.ref.mem != 0) { + //fprintf(stderr, "-->releaseMemObject[%d]\n", i); + status = clReleaseMemObject((cl_mem)arg->value.ref.mem); + //fprintf(stderr, "<--releaseMemObject[%d]\n", i); + ASSERT_CL("clReleaseMemObject()"); + arg->value.ref.mem = (cl_mem)0; + } - if (arg->mustWriteBuffer()){ -#ifdef VERBOSE_EXPLICIT - if (arg->isExplicit() && arg->isExplicitWrite()){ - fprintf(stderr, "writing explicit buffer %d %s\n", i, arg->name); - } -#endif - if (jniContext->isVerbose()){ - fprintf(stderr, "%s writing buffer %d %s\n", (arg->isExplicit() ? "explicitly" : ""), - i, arg->name); - } - if (jniContext->isProfilingEnabled()) { - jniContext->writeEventArgs[writeEventCount]=i; - } + arg->value.ref.mem = (cl_mem) 0; + arg->value.ref.addr = NULL; - status = clEnqueueWriteBuffer(jniContext->commandQueues[0], arg->value.ref.mem, CL_FALSE, 0, - arg->sizeInBytes, arg->value.ref.addr, 0, NULL, &(jniContext->writeEvents[writeEventCount++])); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clEnqueueWriteBuffer"); - jniContext->unpinAll(); - return status; - } - if (arg->isExplicit() && arg->isExplicitWrite()){ - arg->type &= ~com_amd_aparapi_KernelRunner_ARG_EXPLICIT_WRITE; -#ifdef VERBOSE_EXPLICIT - fprintf(stderr, "clearing explicit buffer bit %d %s\n", i, arg->name); -#endif - jenv->SetIntField(arg->javaArg, typeFieldID,arg->type ); - } - } - } else if (arg->isLocal()){ - if (jniContext->firstRun){ - // must multiply perlocalByteSize by localSize to get real opencl buffer size - int bytesPerLocalSize = jenv->GetIntField(arg->javaArg, bytesPerLocalSizeFieldID); - int adjustedLocalBufSize = bytesPerLocalSize * localSize; + // Capture new array ref from the kernel arg object + if (newRef != NULL) { + arg->value.ref.javaArray = (jarray)jenv->NewWeakGlobalRef((jarray)newRef); if (jniContext->isVerbose()){ - fprintf(stderr, "ISLOCAL, clSetKernelArg(jniContext->kernel, %d, %d, NULL);\n", i, adjustedLocalBufSize); - } - status = clSetKernelArg(jniContext->kernel, kernelArgPos++, adjustedLocalBufSize, NULL); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clSetKernelArg() (local)"); - jniContext->unpinAll(); - return status; + fprintf(stderr, "NewWeakGlobalRef for %s, set to %p\n", arg->name, + arg->value.ref.javaArray); } } else { - // Keep the arg position in sync if no updates were required - kernelArgPos++; - if (arg->usesArrayLength()){ - kernelArgPos++; - } + arg->value.ref.javaArray = NULL; } - }else{ // primitive arguments + arg->value.ref.isArray = true; - // we need to reflectively sync the value out of the kernel object - if (arg->isFloat()){ - if (arg->isStatic){ - arg->value.f = jenv->GetStaticFloatField(jniContext->kernelClass, arg->fieldID); - }else{ - arg->value.f = jenv->GetFloatField(jniContext->kernelObject, arg->fieldID); - } - //fprintf(stderr, "float arg %d\n", arg->value.f); - }else if (arg->isInt()){ - if (arg->isStatic){ - arg->value.i = jenv->GetStaticIntField(jniContext->kernelClass, arg->fieldID); - }else{ - arg->value.i = jenv->GetIntField(jniContext->kernelObject, arg->fieldID); - } - //fprintf(stderr, "int arg %d\n", arg->value.i); - }else if (arg->isBoolean()){ - if (arg->isStatic){ - arg->value.c = jenv->GetStaticBooleanField(jniContext->kernelClass, arg->fieldID); - }else{ - arg->value.c = jenv->GetBooleanField(jniContext->kernelObject, arg->fieldID); - } - //fprintf(stderr, "boolean arg %d\n", arg->value.c); - }else if (arg->isByte()){ - if (arg->isStatic){ - arg->value.c = jenv->GetStaticByteField(jniContext->kernelClass, arg->fieldID); - }else{ - arg->value.c = jenv->GetByteField(jniContext->kernelObject, arg->fieldID); - } - //fprintf(stderr, "byte arg %d\n", arg->value.c); - }else if (arg->isLong()){ - if (arg->isStatic){ - arg->value.j = jenv->GetStaticLongField(jniContext->kernelClass, arg->fieldID); - }else{ - arg->value.j = jenv->GetLongField(jniContext->kernelObject, arg->fieldID); - } - //fprintf(stderr, "long arg %d\n", arg->value.c); - }else if (arg->isDouble()){ - if (arg->isStatic){ - arg->value.d = jenv->GetStaticDoubleField(jniContext->kernelClass, arg->fieldID); - }else{ - arg->value.d = jenv->GetDoubleField(jniContext->kernelObject, arg->fieldID); - } - //fprintf(stderr, "double arg %d\n", arg->value.c); - } + // Save the sizeInBytes which was set on the java side + arg->syncSizeInBytes(jenv); if (jniContext->isVerbose()){ - fprintf(stderr, "clSetKernelArg %s: %d %d %d 0x%08x\n", arg->name, i, kernelArgPos, - arg->sizeInBytes, arg->value); - } - status = clSetKernelArg(jniContext->kernel, kernelArgPos++, arg->sizeInBytes, &(arg->value)); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clSetKernelArg() (value)"); - jniContext->unpinAll(); - return status; + fprintf(stderr, "updateNonPrimitiveReferences, args[%d].sizeInBytes=%d\n", i, arg->sizeInBytes); } - } - } // for each arg + } // object has changed + } + } // for each arg + } // if jniContext != NULL + return(status); +} - size_t globalSizeAsSizeT = (globalSize /jniContext->deviceIdc); - size_t localSizeAsSizeT = localSize; - // To support multiple passes we add a 'secret' final arg called 'passid' and just schedule multiple enqueuendrange kernels. Each of which having a separate value of passid - for (int passid=0; passid<passes; passid++){ - for (int dev =0; dev < jniContext->deviceIdc; dev++){ - size_t offset = (size_t)((globalSize/jniContext->deviceIdc)*dev); - status = clSetKernelArg(jniContext->kernel, kernelArgPos, sizeof(passid), &(passid)); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clSetKernelArg() (passid)"); - jniContext->unpinAll(); - return status; - } +JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_runKernelJNI(JNIEnv *jenv, + jobject jobj, jlong jniContextHandle, jobject _range, jboolean needSync, jint passes) { - // four options here due to passid - if (passid == 0 && passes==1){ - //fprintf(stderr, "setting passid to %d of %d first and last\n", passid, passes); - // there is one pass and this is it - // enqueue depends on write enqueues - // we don't block but and we populate the executeEvents - status = clEnqueueNDRangeKernel(jniContext->commandQueues[dev], jniContext->kernel, 1, &offset, &globalSizeAsSizeT, - useNullForLocalSize ? NULL : &localSizeAsSizeT, - writeEventCount, writeEventCount?jniContext->writeEvents:NULL, &jniContext->executeEvents[dev]); - }else if (passid == 0){ - //fprintf(stderr, "setting passid to %d of %d first not last\n", passid, passes); - // this is the first of multiple passes - // enqueue depends on write enqueues - // we block but do not populate executeEvents (only the last pass does this) - status = clEnqueueNDRangeKernel(jniContext->commandQueues[dev], jniContext->kernel, 1, &offset, &globalSizeAsSizeT, - useNullForLocalSize ? NULL : &localSizeAsSizeT, - writeEventCount, writeEventCount?jniContext->writeEvents:NULL, &jniContext->executeEvents[dev]); - - }else if (passid < passes-1){ - // we are in some middle pass (neither first or last) - // we don't depend on write enqueues - // we block and do not supply executeEvents (only the last pass does this) - //fprintf(stderr, "setting passid to %d of %d not first not last\n", passid, passes); - status = clEnqueueNDRangeKernel(jniContext->commandQueues[dev], jniContext->kernel, 1, &offset, &globalSizeAsSizeT, - useNullForLocalSize ? NULL : &localSizeAsSizeT, 0, NULL, &jniContext->executeEvents[dev]); - }else{ - // we are the last pass of >1 - // we don't depend on write enqueues - // we block and supply executeEvents - //fprintf(stderr, "setting passid to %d of %d last\n", passid, passes); - status = clEnqueueNDRangeKernel(jniContext->commandQueues[dev], jniContext->kernel, 1, &offset, &globalSizeAsSizeT, - useNullForLocalSize ? NULL : &localSizeAsSizeT, 0, NULL, &jniContext->executeEvents[dev]); - } + Range range(jenv, _range); + cl_int status = CL_SUCCESS; + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + if (jniContext->isVerbose()){ + timer.start(); + } - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clEnqueueNDRangeKernel()"); - fprintf(stderr, "after clEnqueueNDRangeKernel, globalSize=%d localSize=%d usingNull=%d\n", (int)globalSizeAsSizeT, (int)localSizeAsSizeT, useNullForLocalSize); - jniContext->unpinAll(); - return status; - } - } - if (passid < passes-1){ - // we need to wait for the executions to complete... - status = clWaitForEvents(jniContext->deviceIdc, jniContext->executeEvents); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clWaitForEvents() execute events mid pass"); - jniContext->unpinAll(); - return status; - } + // Need to capture array refs + if (jniContext->firstRun || needSync) { + updateNonPrimitiveReferences(jenv, jobj, jniContext ); + if (jniContext->isVerbose()){ + fprintf(stderr, "back from updateNonPrimitiveReferences\n"); + } + } - for (int dev = 0; dev < jniContext->deviceIdc; dev++){ - status = clReleaseEvent(jniContext->executeEvents[dev]); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clReleaseEvent() read event"); - jniContext->unpinAll(); - return status; - } - } - } + int writeEventCount = 0; + + // kernelArgPos is used to keep track of the kernel arg position, it can + // differ from "i" due to insertion of javaArrayLength args which are not + // fields read from the kernel object. + int kernelArgPos = 0; + + for (int i=0; i< jniContext->argc; i++){ + KernelArg *arg = jniContext->args[i]; + // TODO: see if we can get rid of this read + arg->syncType(jenv); + if (jniContext->isVerbose()){ + fprintf(stderr, "got type for arg %d, %s, type=%08x\n", i, arg->name, arg->type); + } + if (!arg->isPrimitive() && !arg->isLocal()) { + // pin the arrays so that GC does not move them during the call + + // get the C memory address for the region being transferred + // this uses different JNI calls for arrays vs. directBufs + void * prevAddr = arg->value.ref.addr; + if (arg->value.ref.isArray) { + arg->pin(jenv); } - int readEventCount = 0; + if (jniContext->isVerbose()){ + fprintf(stderr, "runKernel: arrayOrBuf ref %p, oldAddr=%p, newAddr=%p, ref.mem=%p, isArray=%d\n", + arg->value.ref.javaArray, + prevAddr, + arg->value.ref.addr, + arg->value.ref.mem, + arg->value.ref.isArray ); + fprintf(stderr, "at memory addr %p, contents: ", arg->value.ref.addr); + unsigned char *pb = (unsigned char *) arg->value.ref.addr; + for (int k=0; k<8; k++) { + fprintf(stderr, "%02x ", pb[k]); + } + fprintf(stderr, "\n" ); + } + // record whether object moved + // if we see that isCopy was returned by getPrimitiveArrayCritical, treat that as a move + bool objectMoved = (arg->value.ref.addr != prevAddr) || arg->value.ref.isCopy; - for (int i=0; i< jniContext->argc; i++){ - KernelArg *arg = jniContext->args[i]; +#ifdef VERBOSE_EXPLICIT + if (arg->isExplicit() && arg->isExplicitWrite()){ + fprintf(stderr, "explicit write of %s\n", arg->name); + } +#endif - if (arg->mustReadBuffer()){ - if (jniContext->isProfilingEnabled()) { - jniContext->readEventArgs[readEventCount]=i; - } - if (jniContext->isVerbose()){ - fprintf(stderr, "reading buffer %d %s\n", i, arg->name); - } + if (jniContext->firstRun || (arg->value.ref.mem == 0) || objectMoved ){ + // if either this is the first run or user changed input array + // or gc moved something, then we create buffers/args + cl_uint mask = CL_MEM_USE_HOST_PTR; + if (arg->isRead() && arg->isWrite()) mask |= CL_MEM_READ_WRITE; + else if (arg->isRead() && !arg->isWrite()) mask |= CL_MEM_READ_ONLY; + else if (arg->isWrite()) mask |= CL_MEM_WRITE_ONLY; + arg->value.ref.memMask = mask; + if (jniContext->isVerbose()){ + strcpy(arg->value.ref.memSpec,"CL_MEM_USE_HOST_PTR"); + if (mask & CL_MEM_READ_WRITE) strcat(arg->value.ref.memSpec,"|CL_MEM_READ_WRITE"); + if (mask & CL_MEM_READ_ONLY) strcat(arg->value.ref.memSpec,"|CL_MEM_READ_ONLY"); + if (mask & CL_MEM_WRITE_ONLY) strcat(arg->value.ref.memSpec,"|CL_MEM_WRITE_ONLY"); - status = clEnqueueReadBuffer(jniContext->commandQueues[0], arg->value.ref.mem, CL_FALSE, 0, - arg->sizeInBytes,arg->value.ref.addr , jniContext->deviceIdc, jniContext->executeEvents, &(jniContext->readEvents[readEventCount++])); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clEnqueueReadBuffer()"); - jniContext->unpinAll(); - return status; - } + fprintf(stderr, "%s %d clCreateBuffer(context, %s, size=%08x bytes, address=%08x, &status)\n", arg->name, + i, arg->value.ref.memSpec, arg->sizeInBytes, arg->value.ref.addr); } - } + arg->value.ref.mem = clCreateBuffer(jniContext->context, arg->value.ref.memMask, + arg->sizeInBytes, arg->value.ref.addr, &status); - // don't change the order here - // We wait for the reads which each depend on the execution, which depends on the writes ;) - // So after the reads have completed, we can release the execute and writes. + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clCreateBuffer"); + jniContext->unpinAll(jenv); + return status; + } - if (readEventCount >0){ - status = clWaitForEvents(readEventCount, jniContext->readEvents); + status = clSetKernelArg(jniContext->kernel, kernelArgPos++, sizeof(cl_mem), (void *)&(arg->value.ref.mem)); if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clWaitForEvents() read events"); - jniContext->unpinAll(); + PRINT_CL_ERR(status, "clSetKernelArg (array)"); + jniContext->unpinAll(jenv); return status; } - for (int i=0; i< readEventCount; i++){ - if (jniContext->isProfilingEnabled()) { - status = profile(&jniContext->args[jniContext->readEventArgs[i]]->value.ref.read, &jniContext->readEvents[i]); - if (status != CL_SUCCESS) { - jniContext->unpinAll(); - return status; - } + // Add the array length if needed + if (arg->usesArrayLength()){ + arg->syncJavaArrayLength(jenv); + + status = clSetKernelArg(jniContext->kernel, kernelArgPos++, sizeof(jint), &(arg->value.ref.javaArrayLength)); + + if (jniContext->isVerbose()){ + fprintf(stderr, "runKernel arg %d %s, javaArrayLength = %d\n", i, arg->name, arg->value.ref.javaArrayLength); } - status = clReleaseEvent(jniContext->readEvents[i]); if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clReleaseEvent() read event"); - jniContext->unpinAll(); + PRINT_CL_ERR(status, "clSetKernelArg (array length)"); + jniContext->unpinAll(jenv); return status; } } } else { - // if readEventCount == 0 then we don't need any reads so we just wait for the executions to complete - status = clWaitForEvents(jniContext->deviceIdc, jniContext->executeEvents); + // Keep the arg position in sync if no updates were required + kernelArgPos++; + if (arg->usesArrayLength()){ + kernelArgPos++; + } + } + + // we only enqueue a write if we know the kernel actually reads the buffer or if there is an explicit write pending + // the default behavior for Constant buffers is also that there is no write enqueued unless explicit + + if (arg->mustWriteBuffer()){ +#ifdef VERBOSE_EXPLICIT + if (arg->isExplicit() && arg->isExplicitWrite()){ + fprintf(stderr, "writing explicit buffer %d %s\n", i, arg->name); + } +#endif + if (jniContext->isVerbose()){ + fprintf(stderr, "%s writing buffer %d %s\n", (arg->isExplicit() ? "explicitly" : ""), + i, arg->name); + } + if (jniContext->isProfilingEnabled()) { + jniContext->writeEventArgs[writeEventCount]=i; + } + + status = clEnqueueWriteBuffer(jniContext->commandQueues[0], arg->value.ref.mem, CL_FALSE, 0, + arg->sizeInBytes, arg->value.ref.addr, 0, NULL, &(jniContext->writeEvents[writeEventCount++])); if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clWaitForEvents() execute event"); - jniContext->unpinAll(); + PRINT_CL_ERR(status, "clEnqueueWriteBuffer"); + jniContext->unpinAll(jenv); return status; } + if (arg->isExplicit() && arg->isExplicitWrite()){ +#ifdef VERBOSE_EXPLICIT + fprintf(stderr, "clearing explicit buffer bit %d %s\n", i, arg->name); +#endif + arg->clearExplicitBufferBit(jenv); + } } + } else if (arg->isLocal()){ + if (jniContext->firstRun){ + int bytes = arg->sizeInBytes; - if (jniContext->isProfilingEnabled()) { - status = profile(&jniContext->exec, &jniContext->executeEvents[0]); + if (jniContext->isVerbose()){ + fprintf(stderr, "ISLOCAL, clSetKernelArg(jniContext->kernel, %d, %d, NULL);\n", i, bytes); + } + status = clSetKernelArg(jniContext->kernel, kernelArgPos++, bytes, NULL); if (status != CL_SUCCESS) { - jniContext->unpinAll(); + PRINT_CL_ERR(status, "clSetKernelArg() (local)"); + jniContext->unpinAll(jenv); return status; } + } else { + // Keep the arg position in sync if no updates were required + kernelArgPos++; + if (arg->usesArrayLength()){ + kernelArgPos++; + } + } + }else{ // primitive arguments + + // we need to reflectively sync the value out of the kernel object + if (arg->isFloat()){ + if (arg->isStatic){ + arg->value.f = jenv->GetStaticFloatField(jniContext->kernelClass, arg->fieldID); + }else{ + arg->value.f = jenv->GetFloatField(jniContext->kernelObject, arg->fieldID); + } + //fprintf(stderr, "float arg %d\n", arg->value.f); + }else if (arg->isInt()){ + if (arg->isStatic){ + arg->value.i = jenv->GetStaticIntField(jniContext->kernelClass, arg->fieldID); + }else{ + arg->value.i = jenv->GetIntField(jniContext->kernelObject, arg->fieldID); + } + //fprintf(stderr, "int arg %d\n", arg->value.i); + }else if (arg->isBoolean()){ + if (arg->isStatic){ + arg->value.c = jenv->GetStaticBooleanField(jniContext->kernelClass, arg->fieldID); + }else{ + arg->value.c = jenv->GetBooleanField(jniContext->kernelObject, arg->fieldID); + } + //fprintf(stderr, "boolean arg %d\n", arg->value.c); + }else if (arg->isByte()){ + if (arg->isStatic){ + arg->value.c = jenv->GetStaticByteField(jniContext->kernelClass, arg->fieldID); + }else{ + arg->value.c = jenv->GetByteField(jniContext->kernelObject, arg->fieldID); + } + //fprintf(stderr, "byte arg %d\n", arg->value.c); + }else if (arg->isLong()){ + if (arg->isStatic){ + arg->value.j = jenv->GetStaticLongField(jniContext->kernelClass, arg->fieldID); + }else{ + arg->value.j = jenv->GetLongField(jniContext->kernelObject, arg->fieldID); + } + //fprintf(stderr, "long arg %d\n", arg->value.c); + }else if (arg->isDouble()){ + if (arg->isStatic){ + arg->value.d = jenv->GetStaticDoubleField(jniContext->kernelClass, arg->fieldID); + }else{ + arg->value.d = jenv->GetDoubleField(jniContext->kernelObject, arg->fieldID); + } + //fprintf(stderr, "double arg %d\n", arg->value.c); + } + + if (jniContext->isVerbose()){ + fprintf(stderr, "clSetKernelArg %s: %d %d %d 0x%08x\n", arg->name, i, kernelArgPos, + arg->sizeInBytes, arg->value); } + status = clSetKernelArg(jniContext->kernel, kernelArgPos++, arg->sizeInBytes, &(arg->value)); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clSetKernelArg() (value)"); + jniContext->unpinAll(jenv); + return status; + } + } + } // for each arg - // extract the execution status from the executeEvent - cl_int executeStatus; - status = clGetEventInfo(jniContext->executeEvents[0], CL_EVENT_COMMAND_EXECUTION_STATUS, sizeof(cl_int), &executeStatus, NULL); + // We will need to revisit the execution of multiple devices. + // POssibly cloning the range per device and mutating each to handle a unique subrange (of global) and + // maybe even pushing the offset into the range class. + + // size_t globalSize_0AsSizeT = (range.globalDims[0] /jniContext->deviceIdc); + // size_t localSize_0AsSizeT = range.localDims[0]; + + // To support multiple passes we add a 'secret' final arg called 'passid' and just schedule multiple enqueuendrange kernels. Each of which having a separate value of passid + // + + for (int passid=0; passid<passes; passid++){ + for (int dev =0; dev < jniContext->deviceIdc; dev++){ // this will always be 1 until we reserect multi-dim support + //size_t offset = 1; // (size_t)((range.globalDims[0]/jniContext->deviceIdc)*dev); + status = clSetKernelArg(jniContext->kernel, kernelArgPos, sizeof(passid), &(passid)); if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clGetEventInfo() execute event"); - jniContext->unpinAll(); + PRINT_CL_ERR(status, "clSetKernelArg() (passid)"); + jniContext->unpinAll(jenv); return status; } - if (executeStatus != CL_SUCCESS) { - // it should definitely not be negative, but since we did a wait above, it had better be CL_COMPLETE==CL_SUCCESS - PRINT_CL_ERR(executeStatus, "Execution status of execute event"); - jniContext->unpinAll(); - return executeStatus; + + // four options here due to passid + if (passid == 0 && passes==1){ + //fprintf(stderr, "setting passid to %d of %d first and last\n", passid, passes); + // there is one pass and this is it + // enqueue depends on write enqueues + // we don't block but and we populate the executeEvents + status = clEnqueueNDRangeKernel( + jniContext->commandQueues[dev], + jniContext->kernel, + range.dims, + range.offsets, range.globalDims, + range.localDims, + writeEventCount, + writeEventCount?jniContext->writeEvents:NULL, + &jniContext->executeEvents[dev]); + }else if (passid == 0){ + //fprintf(stderr, "setting passid to %d of %d first not last\n", passid, passes); + // this is the first of multiple passes + // enqueue depends on write enqueues + // we block but do not populate executeEvents (only the last pass does this) + status = clEnqueueNDRangeKernel( + jniContext->commandQueues[dev], + jniContext->kernel, + range.dims, + range.offsets, + range.globalDims, + range.localDims, + writeEventCount, + writeEventCount?jniContext->writeEvents:NULL, + &jniContext->executeEvents[dev]); + + }else if (passid < passes-1){ + // we are in some middle pass (neither first or last) + // we don't depend on write enqueues + // we block and do not supply executeEvents (only the last pass does this) + //fprintf(stderr, "setting passid to %d of %d not first not last\n", passid, passes); + status = clEnqueueNDRangeKernel( + jniContext->commandQueues[dev], + jniContext->kernel, + range.dims, + range.offsets, + range.globalDims, + range.localDims, + 0, // wait for this event count + NULL, // list of events to wait for + &jniContext->executeEvents[dev]); + }else{ + // we are the last pass of >1 + // we don't depend on write enqueues + // we block and supply executeEvents + //fprintf(stderr, "setting passid to %d of %d last\n", passid, passes); + status = clEnqueueNDRangeKernel( + jniContext->commandQueues[dev], + jniContext->kernel, + range.dims, + range.offsets, + range.globalDims, + range.localDims, + 0, // wait for this event count + NULL, // list of events to wait for + &jniContext->executeEvents[dev]); } - for (int dev=0; dev<jniContext->deviceIdc; dev++){ - status = clReleaseEvent(jniContext->executeEvents[dev]); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clReleaseEvent() execute event"); - jniContext->unpinAll(); - return status; - } + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clEnqueueNDRangeKernel()"); + fprintf(stderr, "after clEnqueueNDRangeKernel, globalSize_0=%d localSize_0=%d\n", (int)range.globalDims[0], range.localDims[0] ); + jniContext->unpinAll(jenv); + return status; + } + } + if (passid < passes-1){ + // we need to wait for the executions to complete... + status = clWaitForEvents(jniContext->deviceIdc, jniContext->executeEvents); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clWaitForEvents() execute events mid pass"); + jniContext->unpinAll(jenv); + return status; } - for (int i=0; i< writeEventCount; i++){ - if (jniContext->isProfilingEnabled()) { - profile(&jniContext->args[jniContext->writeEventArgs[i]]->value.ref.write, &jniContext->writeEvents[i]); - } - status = clReleaseEvent(jniContext->writeEvents[i]); + for (int dev = 0; dev < jniContext->deviceIdc; dev++){ + status = clReleaseEvent(jniContext->executeEvents[dev]); if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clReleaseEvent() write event"); - jniContext->unpinAll(); + PRINT_CL_ERR(status, "clReleaseEvent() read event"); + jniContext->unpinAll(jenv); return status; } } + } + } + + int readEventCount = 0; - jniContext->unpinAll(); + for (int i=0; i< jniContext->argc; i++){ + KernelArg *arg = jniContext->args[i]; + if (arg->mustReadBuffer()){ if (jniContext->isProfilingEnabled()) { - writeProfileInfo(jniContext); + jniContext->readEventArgs[readEventCount]=i; } - - jniContext->firstRun = false; if (jniContext->isVerbose()){ - timer.end("elapsed"); + fprintf(stderr, "reading buffer %d %s\n", i, arg->name); } - //fprintf(stderr, "About to return %d from exec\n", status); - return(status); + status = clEnqueueReadBuffer(jniContext->commandQueues[0], arg->value.ref.mem, CL_FALSE, 0, + arg->sizeInBytes,arg->value.ref.addr , jniContext->deviceIdc, jniContext->executeEvents, &(jniContext->readEvents[readEventCount++])); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clEnqueueReadBuffer()"); + jniContext->unpinAll(jenv); + return status; + } } + } + // don't change the order here + // We wait for the reads which each depend on the execution, which depends on the writes ;) + // So after the reads have completed, we can release the execute and writes. - // we return the JNIContext from here - JNIEXPORT jlong JNICALL Java_com_amd_aparapi_KernelRunner_initJNI(JNIEnv *jenv, jclass clazz, jobject kernelObject, - jint flags, jint numProcessors, - jint maxJTPLocalSize) { - cl_int status = CL_SUCCESS; - JNIContext* jniContext = new JNIContext(jenv, kernelObject, flags, numProcessors, maxJTPLocalSize); - if (jniContext->isValid()){ - return((jlong)jniContext); - }else{ - return(0L); + if (readEventCount >0){ + status = clWaitForEvents(readEventCount, jniContext->readEvents); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clWaitForEvents() read events"); + jniContext->unpinAll(jenv); + return status; + } + + for (int i=0; i< readEventCount; i++){ + if (jniContext->isProfilingEnabled()) { + status = profile(&jniContext->args[jniContext->readEventArgs[i]]->value.ref.read, &jniContext->readEvents[i]); + if (status != CL_SUCCESS) { + jniContext->unpinAll(jenv); + return status; + } + } + status = clReleaseEvent(jniContext->readEvents[i]); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clReleaseEvent() read event"); + jniContext->unpinAll(jenv); + return status; } } + } else { + // if readEventCount == 0 then we don't need any reads so we just wait for the executions to complete + status = clWaitForEvents(jniContext->deviceIdc, jniContext->executeEvents); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clWaitForEvents() execute event"); + jniContext->unpinAll(jenv); + return status; + } + } + if (jniContext->isProfilingEnabled()) { + status = profile(&jniContext->exec, &jniContext->executeEvents[0]); + if (status != CL_SUCCESS) { + jniContext->unpinAll(jenv); + return status; + } + } - JNIEXPORT jlong JNICALL Java_com_amd_aparapi_KernelRunner_buildProgramJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle, jstring source) { - JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); - if (jniContext == NULL){ - return 0; - } + // extract the execution status from the executeEvent + cl_int executeStatus; + status = clGetEventInfo(jniContext->executeEvents[0], CL_EVENT_COMMAND_EXECUTION_STATUS, sizeof(cl_int), &executeStatus, NULL); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clGetEventInfo() execute event"); + jniContext->unpinAll(jenv); + return status; + } + if (executeStatus != CL_SUCCESS) { + // it should definitely not be negative, but since we did a wait above, it had better be CL_COMPLETE==CL_SUCCESS + PRINT_CL_ERR(executeStatus, "Execution status of execute event"); + jniContext->unpinAll(jenv); + return executeStatus; + } - cl_int status = CL_SUCCESS; - const char *sourceChars = jenv->GetStringUTFChars(source, NULL); - CHECK(sourceChars == NULL, "jenv->GetStringUTFChars() returned null" ); - - size_t sourceSize[] = { strlen(sourceChars) }; - jniContext->program = clCreateProgramWithSource( jniContext->context, 1, &sourceChars, sourceSize, &status); - jenv->ReleaseStringUTFChars(source, sourceChars); - ASSERT_CL("clCreateProgramWithSource()"); - - status = clBuildProgram(jniContext->program, jniContext->deviceIdc, jniContext->deviceIds, NULL, NULL, NULL); - - if(status == CL_BUILD_PROGRAM_FAILURE) { - cl_int logStatus; - size_t buildLogSize = 0; - status = clGetProgramBuildInfo(jniContext->program, jniContext->deviceIds[0], - CL_PROGRAM_BUILD_LOG, buildLogSize, NULL, &buildLogSize); - ASSERT_CL("clGetProgramBuildInfo()"); - char * buildLog = new char[buildLogSize]; - CHECK(buildLog == NULL, "Failed to allocate host memory. (buildLog)"); - memset(buildLog, 0, buildLogSize); - status = clGetProgramBuildInfo (jniContext->program, jniContext->deviceIds[0], - CL_PROGRAM_BUILD_LOG, buildLogSize, buildLog, NULL); - ASSERT_CL("clGetProgramBuildInfo()"); - - fprintf(stderr, "clBuildProgram failed"); - fprintf(stderr, "\n************************************************\n"); - fprintf(stderr, "%s", buildLog); - fprintf(stderr, "\n************************************************\n\n\n"); - delete []buildLog; - return(0); - } + for (int dev=0; dev<jniContext->deviceIdc; dev++){ - jniContext->kernel = clCreateKernel(jniContext->program, "run", &status); - ASSERT_CL("clCreateKernel()"); + status = clReleaseEvent(jniContext->executeEvents[dev]); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clReleaseEvent() execute event"); + jniContext->unpinAll(jenv); + return status; + } + } - cl_command_queue_properties queue_props = 0; - if (jniContext->isProfilingEnabled()) { - queue_props |= CL_QUEUE_PROFILING_ENABLE; - } + for (int i=0; i< writeEventCount; i++){ + if (jniContext->isProfilingEnabled()) { + profile(&jniContext->args[jniContext->writeEventArgs[i]]->value.ref.write, &jniContext->writeEvents[i]); + } + status = clReleaseEvent(jniContext->writeEvents[i]); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clReleaseEvent() write event"); + jniContext->unpinAll(jenv); + return status; + } + } - jniContext->commandQueues= new cl_command_queue[jniContext->deviceIdc]; - for (int dev=0; dev < jniContext->deviceIdc; dev++){ - jniContext->commandQueues[dev]=clCreateCommandQueue(jniContext->context, (cl_device_id)jniContext->deviceIds[dev], - queue_props, - &status); - ASSERT_CL("clCreateCommandQueue()"); - } + jniContext->unpinAll(jenv); - if (jniContext->isProfilingEnabled()) { - // compute profile filename + if (jniContext->isProfilingEnabled()) { + writeProfileInfo(jniContext); + } + + jniContext->firstRun = false; + if (jniContext->isVerbose()){ + timer.end("elapsed"); + } + + //fprintf(stderr, "About to return %d from exec\n", status); + return(status); +} + + +// we return the JNIContext from here +JNIEXPORT jlong JNICALL Java_com_amd_aparapi_KernelRunner_initJNI(JNIEnv *jenv, jclass clazz, jobject kernelObject, + jint flags) { + cl_int status = CL_SUCCESS; + JNIContext* jniContext = new JNIContext(jenv, kernelObject, flags); + if (jniContext->isValid()){ + return((jlong)jniContext); + }else{ + return(0L); + } +} + + +JNIEXPORT jlong JNICALL Java_com_amd_aparapi_KernelRunner_buildProgramJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle, jstring source) { + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + if (jniContext == NULL){ + return 0; + } + + cl_int status = CL_SUCCESS; + const char *sourceChars = jenv->GetStringUTFChars(source, NULL); + CHECK(sourceChars == NULL, "jenv->GetStringUTFChars() returned null" ); + + size_t sourceSize[] = { strlen(sourceChars) }; + jniContext->program = clCreateProgramWithSource( jniContext->context, 1, &sourceChars, sourceSize, &status); + jenv->ReleaseStringUTFChars(source, sourceChars); + ASSERT_CL("clCreateProgramWithSource()"); + + status = clBuildProgram(jniContext->program, jniContext->deviceIdc, jniContext->deviceIds, NULL, NULL, NULL); + + if(status == CL_BUILD_PROGRAM_FAILURE) { + cl_int logStatus; + size_t buildLogSize = 0; + status = clGetProgramBuildInfo(jniContext->program, jniContext->deviceIds[0], + CL_PROGRAM_BUILD_LOG, buildLogSize, NULL, &buildLogSize); + ASSERT_CL("clGetProgramBuildInfo()"); + char * buildLog = new char[buildLogSize]; + CHECK(buildLog == NULL, "Failed to allocate host memory. (buildLog)"); + memset(buildLog, 0, buildLogSize); + status = clGetProgramBuildInfo (jniContext->program, jniContext->deviceIds[0], + CL_PROGRAM_BUILD_LOG, buildLogSize, buildLog, NULL); + ASSERT_CL("clGetProgramBuildInfo()"); + + fprintf(stderr, "clBuildProgram failed"); + fprintf(stderr, "\n************************************************\n"); + fprintf(stderr, "%s", buildLog); + fprintf(stderr, "\n************************************************\n\n\n"); + delete []buildLog; + return(0); + } + + jniContext->kernel = clCreateKernel(jniContext->program, "run", &status); + ASSERT_CL("clCreateKernel()"); + + cl_command_queue_properties queue_props = 0; + if (jniContext->isProfilingEnabled()) { + queue_props |= CL_QUEUE_PROFILING_ENABLE; + } + + jniContext->commandQueues= new cl_command_queue[jniContext->deviceIdc]; + for (int dev=0; dev < jniContext->deviceIdc; dev++){ + jniContext->commandQueues[dev]=clCreateCommandQueue(jniContext->context, (cl_device_id)jniContext->deviceIds[dev], + queue_props, + &status); + ASSERT_CL("clCreateCommandQueue()"); + } + + if (jniContext->isProfilingEnabled()) { + // compute profile filename #if defined (_WIN32) - jint pid = GetCurrentProcessId(); + jint pid = GetCurrentProcessId(); #else - pid_t pid = getpid(); + pid_t pid = getpid(); #endif - // indicate cpu or gpu - // timestamp - // kernel name + // indicate cpu or gpu + // timestamp + // kernel name - jclass classMethodAccess = jenv->FindClass("java/lang/Class"); - jmethodID getNameID=jenv->GetMethodID(classMethodAccess,"getName","()Ljava/lang/String;"); - jstring className = (jstring)jenv->CallObjectMethod(jniContext->kernelClass, getNameID); - const char *classNameChars = jenv->GetStringUTFChars(className, NULL); + jclass classMethodAccess = jenv->FindClass("java/lang/Class"); + jmethodID getNameID=jenv->GetMethodID(classMethodAccess,"getName","()Ljava/lang/String;"); + jstring className = (jstring)jenv->CallObjectMethod(jniContext->kernelClass, getNameID); + const char *classNameChars = jenv->GetStringUTFChars(className, NULL); #define TIME_STR_LEN 200 - char timeStr[TIME_STR_LEN]; - struct tm *tmp; - time_t t = time(NULL); - tmp = localtime(&t); - if (tmp == NULL) { - perror("localtime"); - } - //strftime(timeStr, TIME_STR_LEN, "%F.%H%M%S", tmp); %F seemed to cause a core dump - strftime(timeStr, TIME_STR_LEN, "%H%M%S", tmp); - - char* fnameStr = new char[strlen(classNameChars) + strlen(timeStr) + 128]; + char timeStr[TIME_STR_LEN]; + struct tm *tmp; + time_t t = time(NULL); + tmp = localtime(&t); + if (tmp == NULL) { + perror("localtime"); + } + //strftime(timeStr, TIME_STR_LEN, "%F.%H%M%S", tmp); %F seemed to cause a core dump + strftime(timeStr, TIME_STR_LEN, "%H%M%S", tmp); - //sprintf(fnameStr, "%s.%s.%d.%llx\n", classNameChars, timeStr, pid, jniContext); - sprintf(fnameStr, "aparapiprof.%s.%d.%016lx", timeStr, pid, (unsigned long)jniContext); - jenv->ReleaseStringUTFChars(className, classNameChars); + char* fnameStr = new char[strlen(classNameChars) + strlen(timeStr) + 128]; - FILE* profileFile = fopen(fnameStr, "w"); - if (profileFile != NULL) { - jniContext->profileFile = profileFile; - } else { - jniContext->profileFile = stderr; - fprintf(stderr, "Could not open profile data file %s, reverting to stderr\n", fnameStr); - } - delete []fnameStr; - } + //sprintf(fnameStr, "%s.%s.%d.%llx\n", classNameChars, timeStr, pid, jniContext); + sprintf(fnameStr, "aparapiprof.%s.%d.%016lx", timeStr, pid, (unsigned long)jniContext); + jenv->ReleaseStringUTFChars(className, classNameChars); - return((jlong)jniContext); + FILE* profileFile = fopen(fnameStr, "w"); + if (profileFile != NULL) { + jniContext->profileFile = profileFile; + } else { + jniContext->profileFile = stderr; + fprintf(stderr, "Could not open profile data file %s, reverting to stderr\n", fnameStr); } + delete []fnameStr; + } + return((jlong)jniContext); +} - // this is called once when the arg list is first determined for this kernel - JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_setArgsJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle, jobjectArray argArray, jint argc) { - JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); - cl_int status = CL_SUCCESS; - if (jniContext != NULL){ - jniContext->argc = argc; - jniContext->args = new KernelArg*[jniContext->argc]; - jniContext->firstRun = true; - // Step through the array of KernelArg's to capture the type data for the Kernel's data members. - for (jint i=0; i<jniContext->argc; i++){ - KernelArg* arg = jniContext->args[i] = new KernelArg; +// this is called once when the arg list is first determined for this kernel +JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_setArgsJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle, jobjectArray argArray, jint argc) { + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + cl_int status = CL_SUCCESS; + if (jniContext != NULL){ + jniContext->argc = argc; + jniContext->args = new KernelArg*[jniContext->argc]; + jniContext->firstRun = true; + // Step through the array of KernelArg's to capture the type data for the Kernel's data members. + for (jint i=0; i<jniContext->argc; i++){ + + + jobject argObj = jenv->GetObjectArrayElement(argArray, i); + KernelArg* arg = jniContext->args[i] = new KernelArg(jenv, argObj); - jobject argObj = jenv->GetObjectArrayElement(argArray, i); - if (argClazz == 0){ - argClazz = cacheKernelArgFields(jenv, argObj); - } - arg->javaArg = jenv->NewGlobalRef(argObj); // save a global ref to the java Arg Object - - arg->type = jenv->GetIntField(argObj, typeFieldID); - arg->isStatic = jenv->GetBooleanField(argObj, isStaticFieldID); - jstring name = (jstring)jenv->GetObjectField(argObj, nameFieldID); - const char *nameChars = jenv->GetStringUTFChars(name, NULL); - arg->name=strdup(nameChars); - jenv->ReleaseStringUTFChars(name, nameChars); #ifdef VERBOSE_EXPLICIT - if (arg->isExplicit()){ - fprintf(stderr, "%s is explicit!\n", arg->name); - } + if (arg->isExplicit()){ + fprintf(stderr, "%s is explicit!\n", arg->name); + } #endif - if (arg->isPrimitive()) { - // for primitives, we cache the fieldID for that field in the kernel's arg object - if (arg->isFloat()){ - if (arg->isStatic){ - arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "F"); - }else{ - arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "F"); - } - arg->sizeInBytes = sizeof(jfloat); - }else if (arg->isInt()){ - if (arg->isStatic){ - arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "I"); - }else{ - arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "I"); - } - arg->sizeInBytes = sizeof(jint); - }else if (arg->isByte()){ - if (arg->isStatic){ - arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "B"); - }else{ - arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "B"); - } - arg->sizeInBytes = sizeof(jbyte); - }else if (arg->isBoolean()){ - if (arg->isStatic){ - arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "Z"); - }else{ - arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "Z"); - } - arg->sizeInBytes = sizeof(jboolean); - }else if (arg->isLong()){ - if (arg->isStatic){ - arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "J"); - }else{ - arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "J"); - } - arg->sizeInBytes = sizeof(jlong); - }else if (arg->isDouble()){ - if (arg->isStatic){ - arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "D"); - }else{ - arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "D"); - } - arg->sizeInBytes = sizeof(jdouble); - } - }else{ // we will use an array - arg->value.ref.mem = (cl_mem) 0; - arg->value.ref.javaArray = 0; - arg->sizeInBytes = 0; + if (arg->isPrimitive()) { + // for primitives, we cache the fieldID for that field in the kernel's arg object + if (arg->isFloat()){ + if (arg->isStatic){ + arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "F"); + }else{ + arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "F"); } - if (jniContext->isVerbose()){ - fprintf(stderr, "in setArgs arg %d %s type %08x\n", i, arg->name, arg->type); + arg->sizeInBytes = sizeof(jfloat); + }else if (arg->isInt()){ + if (arg->isStatic){ + arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "I"); + }else{ + arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "I"); } - - //If an error occurred, return early so we report the first problem, not the last - if (jniContext->jenv->ExceptionCheck() == JNI_TRUE) { - jniContext->argc = -1; - delete[] jniContext->args; - jniContext->args = NULL; - jniContext->firstRun = true; - return (status); + arg->sizeInBytes = sizeof(jint); + }else if (arg->isByte()){ + if (arg->isStatic){ + arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "B"); + }else{ + arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "B"); } - - } - // we will need an executeEvent buffer for all devices - jniContext->executeEvents = new cl_event[jniContext->deviceIdc]; - - // We will need *at most* jniContext->argc read/write events - jniContext->readEvents = new cl_event[jniContext->argc]; - if (jniContext->isProfilingEnabled()) { - jniContext->readEventArgs = new jint[jniContext->argc]; - } - jniContext->writeEvents = new cl_event[jniContext->argc]; - if (jniContext->isProfilingEnabled()) { - jniContext->writeEventArgs = new jint[jniContext->argc]; + arg->sizeInBytes = sizeof(jbyte); + }else if (arg->isBoolean()){ + if (arg->isStatic){ + arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "Z"); + }else{ + arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "Z"); + } + arg->sizeInBytes = sizeof(jboolean); + }else if (arg->isLong()){ + if (arg->isStatic){ + arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "J"); + }else{ + arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "J"); + } + arg->sizeInBytes = sizeof(jlong); + }else if (arg->isDouble()){ + if (arg->isStatic){ + arg->fieldID = jenv->GetStaticFieldID(jniContext->kernelClass, arg->name, "D"); + }else{ + arg->fieldID = jenv->GetFieldID(jniContext->kernelClass, arg->name, "D"); + } + arg->sizeInBytes = sizeof(jdouble); } + }else{ // we will use an array + arg->value.ref.mem = (cl_mem) 0; + arg->value.ref.javaArray = 0; + arg->sizeInBytes = 0; } - return(status); - } - - JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_getLocalSizeJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle, jint globalSize, jint localBytesPerLocalId) { - size_t kernelMaxWorkGroupSize = 0; - size_t kernelWorkGroupSizeMultiple = 0; - cl_int status = CL_SUCCESS; - JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); - if (jniContext != NULL){ - clGetKernelWorkGroupInfo(jniContext->kernel, jniContext->deviceIds[0], CL_KERNEL_WORK_GROUP_SIZE, sizeof(kernelMaxWorkGroupSize), &kernelMaxWorkGroupSize, NULL); - ASSERT_CL("clGetKernelWorkGroupInfo()"); - // starting value depends on device type - // not sure why the CPU has a different starting size, but it does - int startLocalSize = (jniContext->deviceType == CL_DEVICE_TYPE_GPU ? kernelMaxWorkGroupSize : globalSize/(jniContext->numProcessors*4)); - - if (startLocalSize == 0) startLocalSize = 1; - if (startLocalSize > kernelMaxWorkGroupSize) startLocalSize = kernelMaxWorkGroupSize; - if (startLocalSize > globalSize) startLocalSize = globalSize; - // if the kernel uses any local memory, determine our max local memory size so we can possibly limit localsize - cl_ulong devLocalMemSize; - if (localBytesPerLocalId > 0) { - status = clGetDeviceInfo(jniContext->deviceIds[0], CL_DEVICE_LOCAL_MEM_SIZE, sizeof(cl_ulong), &devLocalMemSize, NULL); - cl_uint localSizeLimitFromLocalMem = devLocalMemSize/localBytesPerLocalId; - if (startLocalSize > localSizeLimitFromLocalMem) startLocalSize = localSizeLimitFromLocalMem; - if (jniContext->isVerbose()){ - fprintf(stderr, "localBytesPerLocalId=%d, device localMemMax=%d, localSizeLimitFromLocalMem=%d\n", - localBytesPerLocalId, (cl_uint) devLocalMemSize, localSizeLimitFromLocalMem); - } - + if (jniContext->isVerbose()){ + fprintf(stderr, "in setArgs arg %d %s type %08x\n", i, arg->name, arg->type); + if (arg->isLocal()){ + fprintf(stderr, "in setArgs arg %d %s is local\n", i, arg->name); + }else{ + fprintf(stderr, "in setArgs arg %d %s is *not* local\n", i, arg->name); } + } - // then iterate down until we find a localSize that divides globalSize equally - for (int localSize = startLocalSize; localSize>0; localSize--) { - if (globalSize % localSize == 0) { - if (jniContext->isVerbose()){ - fprintf(stderr, "for globalSize=%d, stepping localSize from %d, returning localSize=%d\n", globalSize, startLocalSize, localSize); - } - return localSize; - } - } + //If an error occurred, return early so we report the first problem, not the last + if (jenv->ExceptionCheck() == JNI_TRUE) { + jniContext->argc = -1; + delete[] jniContext->args; + jniContext->args = NULL; + jniContext->firstRun = true; + return (status); } - // should never get this far - return 0; + } + // we will need an executeEvent buffer for all devices + jniContext->executeEvents = new cl_event[jniContext->deviceIdc]; + + // We will need *at most* jniContext->argc read/write events + jniContext->readEvents = new cl_event[jniContext->argc]; + if (jniContext->isProfilingEnabled()) { + jniContext->readEventArgs = new jint[jniContext->argc]; + } + jniContext->writeEvents = new cl_event[jniContext->argc]; + if (jniContext->isProfilingEnabled()) { + jniContext->writeEventArgs = new jint[jniContext->argc]; + } + } + return(status); +} - JNIEXPORT jstring JNICALL Java_com_amd_aparapi_KernelRunner_getExtensions(JNIEnv *jenv, jobject jobj, jlong jniContextHandle) { - jstring jextensions = NULL; - JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); - if (jniContext != NULL){ - size_t retvalsize = 0; - cl_int status = CL_SUCCESS; - status = clGetDeviceInfo(jniContext->deviceIds[0], CL_DEVICE_EXTENSIONS, 0, NULL, &retvalsize); - ASSERT_CL("clGetDeviceInfo()"); - char* extensions = new char[retvalsize]; - clGetDeviceInfo(jniContext->deviceIds[0], CL_DEVICE_EXTENSIONS, retvalsize, extensions, NULL); - jextensions = jenv->NewStringUTF(extensions); - delete [] extensions; - } - return jextensions; - } - - // Called as a result of Kernel.get(someArray) - JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_getJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle, jobject buffer) { - cl_int status = CL_SUCCESS; - JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); - if (jniContext != NULL){ - jboolean foundArg = false; - for (jint i=0; i<jniContext->argc; i++){ - KernelArg *arg= jniContext->args[i]; - if (arg->isArray()){ - jboolean isSame = jenv->IsSameObject(buffer, arg->value.ref.javaArray); - // only do this if the array that we are passed is indeed an arg we are tracking - if (isSame){ - foundArg = true; - //fprintf(stderr, "get of %s\n", arg->name); + + +JNIEXPORT jstring JNICALL Java_com_amd_aparapi_KernelRunner_getExtensionsJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle) { + jstring jextensions = NULL; + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + if (jniContext != NULL){ + size_t retvalsize = 0; + cl_int status = CL_SUCCESS; + status = clGetDeviceInfo(jniContext->deviceIds[0], CL_DEVICE_EXTENSIONS, 0, NULL, &retvalsize); + ASSERT_CL("clGetDeviceInfo()"); + char* extensions = new char[retvalsize]; + clGetDeviceInfo(jniContext->deviceIds[0], CL_DEVICE_EXTENSIONS, retvalsize, extensions, NULL); + jextensions = jenv->NewStringUTF(extensions); + delete [] extensions; + } + return jextensions; +} + +// Called as a result of Kernel.get(someArray) +JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_getJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle, jobject buffer) { + cl_int status = CL_SUCCESS; + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + if (jniContext != NULL){ + jboolean foundArg = false; + for (jint i=0; i<jniContext->argc; i++){ + KernelArg *arg= jniContext->args[i]; + if (arg->isArray()){ + jboolean isSame = jenv->IsSameObject(buffer, arg->value.ref.javaArray); + // only do this if the array that we are passed is indeed an arg we are tracking + if (isSame){ + foundArg = true; + //fprintf(stderr, "get of %s\n", arg->name); #ifdef VERBOSE_EXPLICIT - fprintf(stderr, "explicitly reading buffer %d %s\n", i, arg->name); + fprintf(stderr, "explicitly reading buffer %d %s\n", i, arg->name); #endif - arg->pin(jenv); + arg->pin(jenv); - status = clEnqueueReadBuffer(jniContext->commandQueues[0], arg->value.ref.mem, CL_FALSE, 0, - arg->sizeInBytes,arg->value.ref.addr , 0, NULL, &jniContext->readEvents[0]); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clEnqueueReadBuffer()"); - return status; - } - status = clWaitForEvents(1, jniContext->readEvents); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clWaitForEvents"); - return status; - } - clReleaseEvent(jniContext->readEvents[0]); - if (status != CL_SUCCESS) { - PRINT_CL_ERR(status, "clReleaseEvent() read event"); - return status; - } - // since this is an explicit buffer get, we expect the buffer to have changed so we commit - arg->unpinCommit(jenv); - } + status = clEnqueueReadBuffer(jniContext->commandQueues[0], arg->value.ref.mem, CL_FALSE, 0, + arg->sizeInBytes,arg->value.ref.addr , 0, NULL, &jniContext->readEvents[0]); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clEnqueueReadBuffer()"); + return status; } - } - if (!foundArg){ - if (jniContext->isVerbose()){ - fprintf(stderr, "attempt to request to get a buffer that does not appear to be referenced from kernel\n"); + status = clWaitForEvents(1, jniContext->readEvents); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clWaitForEvents"); + return status; } + clReleaseEvent(jniContext->readEvents[0]); + if (status != CL_SUCCESS) { + PRINT_CL_ERR(status, "clReleaseEvent() read event"); + return status; + } + // since this is an explicit buffer get, we expect the buffer to have changed so we commit + arg->unpinCommit(jenv); } } - return 0; } + if (!foundArg){ + if (jniContext->isVerbose()){ + fprintf(stderr, "attempt to request to get a buffer that does not appear to be referenced from kernel\n"); + } + } + } + return 0; +} + +JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_getMaxComputeUnitsJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle) { + cl_int status = CL_SUCCESS; + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + if (jniContext != NULL){ + return(jniContext->maxComputeUnits); + }else{ + return(0); + } +} +JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_getMaxWorkItemDimensionsJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle) { + cl_int status = CL_SUCCESS; + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + if (jniContext != NULL){ + return(jniContext->maxWorkItemDimensions); + }else{ + return(0); + } +} +JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_getMaxWorkGroupSizeJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle) { + cl_int status = CL_SUCCESS; + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + if (jniContext != NULL){ + return(jniContext->maxWorkGroupSize); + }else{ + return(0); + } +} + +JNIEXPORT jint JNICALL Java_com_amd_aparapi_KernelRunner_getMaxWorkItemSizeJNI(JNIEnv *jenv, jobject jobj, jlong jniContextHandle, jint _index) { + cl_int status = CL_SUCCESS; + JNIContext* jniContext = JNIContext::getJNIContext(jniContextHandle); + if (jniContext != NULL && _index >=0 && _index <= jniContext->maxWorkItemDimensions){ + return(jniContext->maxWorkItemSizes[_index]); + }else{ + return(0); + } +} diff --git a/com.amd.aparapi/build.xml b/com.amd.aparapi/build.xml index c57f0cb3..099eee30 100644 --- a/com.amd.aparapi/build.xml +++ b/com.amd.aparapi/build.xml @@ -8,7 +8,7 @@ <delete file="aparapi.jar"/> </target> - <target name="build"> + <target name="build" depends="clean"> <mkdir dir="classes"/> <javac destdir="classes" debug="on" includeAntRuntime="false" > <src path="src/java"/> diff --git a/com.amd.aparapi/src/java/com/amd/aparapi/ClassModel.java b/com.amd.aparapi/src/java/com/amd/aparapi/ClassModel.java index 57f25935..8d6b45b4 100644 --- a/com.amd.aparapi/src/java/com/amd/aparapi/ClassModel.java +++ b/com.amd.aparapi/src/java/com/amd/aparapi/ClassModel.java @@ -2090,9 +2090,9 @@ class ClassModel{ } public boolean isStatic() { - return (Access.STATIC.bitIsSet(methodAccessFlags)); + return (Access.STATIC.bitIsSet(methodAccessFlags)); } - + AttributePool getAttributePool() { return (methodAttributePool); } diff --git a/com.amd.aparapi/src/java/com/amd/aparapi/Config.java b/com.amd.aparapi/src/java/com/amd/aparapi/Config.java index 46d2266a..192edeac 100644 --- a/com.amd.aparapi/src/java/com/amd/aparapi/Config.java +++ b/com.amd.aparapi/src/java/com/amd/aparapi/Config.java @@ -175,6 +175,7 @@ class Config{ static String instructionListenerClassName = System.getProperty(propPkgName + ".instructionListenerClass"); static public InstructionListener instructionListener = null; + { if (instructionListenerClassName != null && !instructionListenerClassName.equals("")) { diff --git a/com.amd.aparapi/src/java/com/amd/aparapi/DeprecatedException.java b/com.amd.aparapi/src/java/com/amd/aparapi/DeprecatedException.java new file mode 100644 index 00000000..70ca856e --- /dev/null +++ b/com.amd.aparapi/src/java/com/amd/aparapi/DeprecatedException.java @@ -0,0 +1,46 @@ +/* +Copyright (c) 2010-2011, Advanced Micro Devices, Inc. +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 the copyright holder 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 HOLDER 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. + +If you use the software (in whole or in part), you shall adhere to all applicable U.S., European, and other export +laws, including but not limited to the U.S. Export Administration Regulations ("EAR"), (15 C.F.R. Sections 730 through +774), and E.U. Council Regulation (EC) No 1334/2000 of 22 June 2000. Further, pursuant to Section 740.6 of the EAR, +you hereby certify that, except pursuant to a license granted by the United States Department of Commerce Bureau of +Industry and Security or as otherwise permitted pursuant to a License Exception under the U.S. Export Administration +Regulations ("EAR"), you will not (1) export, re-export or release to a national of a country in Country Groups D:1, +E:1 or E:2 any restricted technology, software, or source code you receive hereunder, or (2) export to Country Groups +D:1, E:1 or E:2 the direct product of such technology or software, if such foreign produced direct product is subject +to national security controls as identified on the Commerce Control List (currently found in Supplement 1 to Part 774 +of EAR). For the most current Country Group listings, or for additional information about the EAR or your obligations +under those regulations, please refer to the U.S. Bureau of Industry and Security's website at http://www.bis.doc.gov/. + +*/ +package com.amd.aparapi; + +@SuppressWarnings("serial") class DeprecatedException extends AparapiException{ + + DeprecatedException(String msg) { + super(msg); + } + +} diff --git a/com.amd.aparapi/src/java/com/amd/aparapi/Kernel.java b/com.amd.aparapi/src/java/com/amd/aparapi/Kernel.java index c6dd14a1..abef24b2 100644 --- a/com.amd.aparapi/src/java/com/amd/aparapi/Kernel.java +++ b/com.amd.aparapi/src/java/com/amd/aparapi/Kernel.java @@ -89,16 +89,17 @@ import com.amd.aparapi.ClassModel.ConstantPool.MethodReferenceEntry; * } * </pre></blockquote> * <p> - * To execute this kernel, first create a new instance of it and then call <code>execute(int globalSize)</code>. + * To execute this kernel, first create a new instance of it and then call <code>execute(Range _range)</code>. * <p> * <blockquote><pre> * int[] values = new int[1024]; * // fill values array + * Range range = Range.create(values.length); // create a range 0..1024 * SquareKernel kernel = new SquareKernel(values); - * kernel.execute(values.length); + * kernel.execute(range); * </pre></blockquote> * <p> - * When <code>execute()</code> returns, all the executions of Kernel.run() have completed and the results are available in the <code>squares</code> array. + * When <code>execute(Range)</code> returns, all the executions of <code>Kernel.run()</code> have completed and the results are available in the <code>squares</code> array. * <p> * <blockquote><pre> * int[] squares = kernel.getSquares(); @@ -110,16 +111,19 @@ import com.amd.aparapi.ClassModel.ConstantPool.MethodReferenceEntry; * A different approach to creating kernels that avoids extending Kernel is to write an anonymous inner class: * <p> * <blockquote><pre> + * * final int[] values = new int[1024]; - * // fill values array + * // fill the values array * final int[] squares = new int[values.length]; + * final Range range = Range.create(values.length); + * * Kernel kernel = new Kernel(){ * public void run() { * int gid = getGlobalID(); * squares[gid] = values[gid]*values[gid]; * } * }; - * kernel.execute(values.length); + * kernel.execute(range); * for (int i=0; i< values.length; i++){ * System.out.printf("%4d %4d %8d\n", i, values[i], squares[i]); * } @@ -141,15 +145,52 @@ public abstract class Kernel implements Cloneable{ } @Retention(RetentionPolicy.RUNTIME) @interface OpenCLDelegate { + } + /** + * We can use this Annotation to 'tag' intended local buffers. + * + * So we can either annotate the buffer + * <pre><code> + * @Local int[] buffer = new int[1024]; + * </code></pre> + * Or use a special suffix + * <pre><code> + * int[] buffer_$local$ = new int[1024]; + * </code></pre> + * + * @see LOCAL_SUFFIX + * + * + */ + public @Retention(RetentionPolicy.RUNTIME) @interface Local { + + } + + /** + * We can use this suffix to 'tag' intended local buffers. + * + * + * So either name the buffer + * <pre><code> + * int[] buffer_$local$ = new int[1024]; + * </code></pre> + * Or use the Annotation form + * <pre><code> + * @Local int[] buffer = new int[1024]; + * </code></pre> + */ + + final static String LOCAL_SUFFIX = "_$local$"; + private static Logger logger = Logger.getLogger(Config.getLoggerName()); public abstract class Entry{ public abstract void run(); - public Kernel execute(int _globalSize) { - return (Kernel.this.execute("foo", _globalSize, 1)); + public Kernel execute(Range _range) { + return (Kernel.this.execute("foo", _range, 1)); } } @@ -292,26 +333,30 @@ public abstract class Kernel implements Cloneable{ private EXECUTION_MODE executionMode = EXECUTION_MODE.getDefaultExecutionMode(); - private int globalId; - - private int localId; - - private int localSize; + int[] globalId = new int[] { + 0, + 0, + 0 + }; - private int globalSize; + int[] localId = new int[] { + 0, + 0, + 0 + }; - private int groupId; + int[] groupId = new int[] { + 0, + 0, + 0 + }; - private int passId; + Range range; - private int numGroups; + int passId; volatile CyclicBarrier localBarrier; - void setGlobalId(int _globalId) { - globalId = _globalId; - } - /** * Determine the globalId of an executing kernel. * <p> @@ -349,19 +394,26 @@ public abstract class Kernel implements Cloneable{ */ @OpenCLDelegate protected final int getGlobalId() { - return (globalId); + return (getGlobalId(0)); } - void setGroupId(int _groupId) { - groupId = _groupId; - + @OpenCLDelegate protected final int getGlobalId(int _dim) { + return (globalId[_dim]); } - void setPassId(int _passId) { - passId = _passId; + /* + @OpenCLDelegate protected final int getGlobalX() { + return (getGlobalId(0)); + } - } + @OpenCLDelegate protected final int getGlobalY() { + return (getGlobalId(1)); + } + @OpenCLDelegate protected final int getGlobalZ() { + return (getGlobalId(2)); + } + */ /** * Determine the groupId of an executing kernel. * <p> @@ -394,9 +446,26 @@ public abstract class Kernel implements Cloneable{ * @return The groupId for this Kernel being executed */ @OpenCLDelegate protected final int getGroupId() { - return (groupId); + return (getGroupId(0)); } + @OpenCLDelegate protected final int getGroupId(int _dim) { + return (groupId[_dim]); + } + + /* + @OpenCLDelegate protected final int getGroupX() { + return (getGroupId(0)); + } + + @OpenCLDelegate protected final int getGroupY() { + return (getGroupId(1)); + } + + @OpenCLDelegate protected final int getGroupZ() { + return (getGroupId(2)); + } + */ /** * Determine the passId of an executing kernel. * <p> @@ -416,10 +485,6 @@ public abstract class Kernel implements Cloneable{ return (passId); } - void setLocalId(int _localId) { - localId = _localId; - } - /** * Determine the local id of an executing kernel. * <p> @@ -451,9 +516,26 @@ public abstract class Kernel implements Cloneable{ * @return The local id for this Kernel being executed */ @OpenCLDelegate protected final int getLocalId() { - return (localId); + return (getLocalId(0)); } + @OpenCLDelegate protected final int getLocalId(int _dim) { + return (localId[_dim]); + } + + /* + @OpenCLDelegate protected final int getLocalX() { + return (getLocalId(0)); + } + + @OpenCLDelegate protected final int getLocalY() { + return (getLocalId(1)); + } + + @OpenCLDelegate protected final int getLocalZ() { + return (getLocalId(2)); + } + */ /** * Determine the size of the group that an executing kernel is a member of. * <p> @@ -472,9 +554,26 @@ public abstract class Kernel implements Cloneable{ * @return The size of the currently executing group. */ @OpenCLDelegate protected final int getLocalSize() { - return (localSize); + return (range.getLocalSize(0)); + } + + @OpenCLDelegate protected final int getLocalSize(int _dim) { + return (range.getLocalSize(_dim)); } + /* + @OpenCLDelegate protected final int getLocalWidth() { + return (range.getLocalSize(0)); + } + + @OpenCLDelegate protected final int getLocalHeight() { + return (range.getLocalSize(1)); + } + + @OpenCLDelegate protected final int getLocalDepth() { + return (range.getLocalSize(2)); + } + */ /** * Determine the value that was passed to <code>Kernel.execute(int globalSize)</code> method. * @@ -486,14 +585,26 @@ public abstract class Kernel implements Cloneable{ * @return The value passed to <code>Kernel.execute(int globalSize)</code> causing the current execution. */ @OpenCLDelegate protected final int getGlobalSize() { - return (globalSize); + return (range.getGlobalSize(0)); } - void setNumGroups(int _numGroups) { - numGroups = _numGroups; - + @OpenCLDelegate protected final int getGlobalSize(int _dim) { + return (range.getGlobalSize(_dim)); } + /* + @OpenCLDelegate protected final int getGlobalWidth() { + return (range.getGlobalSize(0)); + } + + @OpenCLDelegate protected final int getGlobalHeight() { + return (range.getGlobalSize(1)); + } + + @OpenCLDelegate protected final int getGlobalDepth() { + return (range.getGlobalSize(2)); + } + */ /** * Determine the number of groups that will be used to execute a kernel * <p> @@ -509,9 +620,26 @@ public abstract class Kernel implements Cloneable{ * @return The number of groups that kernels will be dispatched into. */ @OpenCLDelegate protected final int getNumGroups() { - return (numGroups); + return (range.getNumGroups(0)); + } + + @OpenCLDelegate protected final int getNumGroups(int _dim) { + return (range.getNumGroups(_dim)); } + /* + @OpenCLDelegate protected final int getNumGroupsWidth() { + return (range.getGroups(0)); + } + + @OpenCLDelegate protected final int getNumGroupsHeight() { + return (range.getGroups(1)); + } + + @OpenCLDelegate protected final int getNumGroupsDepth() { + return (range.getGroups(2)); + } + */ /** * The entry point of a kernel. * @@ -529,9 +657,21 @@ public abstract class Kernel implements Cloneable{ @Override protected Object clone() { try { Kernel worker = (Kernel) super.clone(); - // if there are any private buffers, go thru the fields here - // and allocate a new instance for each clone - + worker.groupId = new int[] { + 0, + 0, + 0 + }; + worker.localId = new int[] { + 0, + 0, + 0 + }; + worker.globalId = new int[] { + 0, + 0, + 0 + }; return worker; } catch (CloneNotSupportedException e) { // TODO Auto-generated catch block @@ -1373,23 +1513,12 @@ public abstract class Kernel implements Cloneable{ * Java version is identical to localBarrier() * * @annotion Experimental + * @deprecated */ - @OpenCLDelegate @Annotations.Experimental protected final void globalBarrier() { - try { - localBarrier.await(); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - final void setSizes(int _globalSize, int _localSize) { - localSize = _localSize; - globalSize = _globalSize; + @OpenCLDelegate @Annotations.Experimental @Deprecated() protected final void globalBarrier() throws DeprecatedException { + throw new DeprecatedException( + "Kernel.globalBarrier() has been deprecated. It was based an incorrect understanding of OpenCL functionality."); } @@ -1441,23 +1570,38 @@ public abstract class Kernel implements Cloneable{ } /** - * Start execution of <code>globalSize</code> kernels. + * Start execution of <code>_range</code> kernels. * <p> * When <code>kernel.execute(globalSize)</code> is invoked, Aparapi will schedule the execution of <code>globalSize</code> kernels. If the execution mode is GPU then * the kernels will execute as OpenCL code on the GPU device. Otherwise, if the mode is JTP, the kernels will execute as a pool of Java threads on the CPU. * <p> - * @param _globalSize The number of Kernels that we would like to initiate. + * @param range The number of Kernels that we would like to initiate. + * @returnThe Kernel instance (this) so we can chain calls to put(arr).execute(range).get(arr) + * + */ + public synchronized Kernel execute(Range _range) { + return (execute(_range, 1)); + } + + /** + * Start execution of <code>_range</code> kernels. + * <p> + * When <code>kernel.execute(_range)</code> is invoked, Aparapi will schedule the execution of <code>_range</code> kernels. If the execution mode is GPU then + * the kernels will execute as OpenCL code on the GPU device. Otherwise, if the mode is JTP, the kernels will execute as a pool of Java threads on the CPU. + * <p> + * Since adding the new <code>Range class</code> this method offers backward compatibility and merely defers to <code> return (execute(Range.create(_range), 1));</code>. + * @param _range The number of Kernels that we would like to initiate. * @returnThe Kernel instance (this) so we can chain calls to put(arr).execute(range).get(arr) * */ - public synchronized Kernel execute(int _globalSize) { - return (execute(_globalSize, 1)); + public synchronized Kernel execute(int _range) { + return (execute(Range.create(_range), 1)); } /** - * Start execution of <code>_passes</code> iterations of <code>globalSize</code> kernels. + * Start execution of <code>_passes</code> iterations of <code>_range</code> kernels. * <p> - * When <code>kernel.execute(globalSize, passes)</code> is invoked, Aparapi will schedule the execution of <code>globalSize</code> kernels. If the execution mode is GPU then + * When <code>kernel.execute(_range, _passes)</code> is invoked, Aparapi will schedule the execution of <code>_reange</code> kernels. If the execution mode is GPU then * the kernels will execute as OpenCL code on the GPU device. Otherwise, if the mode is JTP, the kernels will execute as a pool of Java threads on the CPU. * <p> * @param _globalSize The number of Kernels that we would like to initiate. @@ -1465,8 +1609,23 @@ public abstract class Kernel implements Cloneable{ * @return The Kernel instance (this) so we can chain calls to put(arr).execute(range).get(arr) * */ - public synchronized Kernel execute(int _globalSize, int _passes) { - return (execute("run", _globalSize, _passes)); + public synchronized Kernel execute(Range _range, int _passes) { + return (execute("run", _range, _passes)); + } + + /** + * Start execution of <code>_passes</code> iterations over the <code>_range</code> of kernels. + * <p> + * When <code>kernel.execute(_range)</code> is invoked, Aparapi will schedule the execution of <code>_range</code> kernels. If the execution mode is GPU then + * the kernels will execute as OpenCL code on the GPU device. Otherwise, if the mode is JTP, the kernels will execute as a pool of Java threads on the CPU. + * <p> + * Since adding the new <code>Range class</code> this method offers backward compatibility and merely defers to <code> return (execute(Range.create(_range), 1));</code>. + * @param _range The number of Kernels that we would like to initiate. + * @returnThe Kernel instance (this) so we can chain calls to put(arr).execute(range).get(arr) + * + */ + public synchronized Kernel execute(int _range, int _passes) { + return (execute(Range.create(_range), _passes)); } /** @@ -1480,12 +1639,12 @@ public abstract class Kernel implements Cloneable{ * @return The Kernel instance (this) so we can chain calls to put(arr).execute(range).get(arr) * */ - public synchronized Kernel execute(Entry _entry, int _globalSize) { + public synchronized Kernel execute(Entry _entry, Range _range) { if (kernelRunner == null) { kernelRunner = new KernelRunner(this); } - return (kernelRunner.execute(_entry, _globalSize, 1)); + return (kernelRunner.execute(_entry, _range, 1)); } /** @@ -1499,8 +1658,8 @@ public abstract class Kernel implements Cloneable{ * @return The Kernel instance (this) so we can chain calls to put(arr).execute(range).get(arr) * */ - public synchronized Kernel execute(String _entrypoint, int _globalSize) { - return (execute(_entrypoint, _globalSize, 1)); + public synchronized Kernel execute(String _entrypoint, Range _range) { + return (execute(_entrypoint, _range, 1)); } @@ -1515,12 +1674,12 @@ public abstract class Kernel implements Cloneable{ * @return The Kernel instance (this) so we can chain calls to put(arr).execute(range).get(arr) * */ - public synchronized Kernel execute(String _entrypoint, int _globalSize, int _passes) { + public synchronized Kernel execute(String _entrypoint, Range _range, int _passes) { if (kernelRunner == null) { kernelRunner = new KernelRunner(this); } - return (kernelRunner.execute(_entrypoint, _globalSize, _passes)); + return (kernelRunner.execute(_entrypoint, _range, _passes)); } @@ -1693,11 +1852,6 @@ public abstract class Kernel implements Cloneable{ return (false); } - void setLocalSize(int _localSize) { - localSize = _localSize; - - } - // the flag useNullForLocalSize is useful for testing that what we compute for localSize is what OpenCL // would also compute if we passed in null. In non-testing mode, we just call execute with the // same localSize that we computed in getLocalSizeJNI. We don't want do publicize these of course. diff --git a/com.amd.aparapi/src/java/com/amd/aparapi/KernelRunner.java b/com.amd.aparapi/src/java/com/amd/aparapi/KernelRunner.java index 87e7de46..ab2f07f5 100644 --- a/com.amd.aparapi/src/java/com/amd/aparapi/KernelRunner.java +++ b/com.amd.aparapi/src/java/com/amd/aparapi/KernelRunner.java @@ -70,6 +70,9 @@ import com.amd.aparapi.Kernel.EXECUTION_MODE; * */ class KernelRunner{ + /** + * Be careful changing the name/type of this field as it is referenced from JNI code. + */ public @interface UsedByJNICode { } @@ -305,7 +308,7 @@ class KernelRunner{ * * @author gfrost */ - @UsedByJNICode public static final int ARG_APARAPI_BUF_IS_DIRECT = 1 << 20; + // @UsedByJNICode public static final int ARG_APARAPI_BUF_IS_DIRECT = 1 << 20; /** * This 'bit' indicates that a particular <code>KernelArg</code> represents a <code>char</code> type (array or primitive). @@ -447,7 +450,7 @@ class KernelRunner{ * * At present only set for AparapiLocalBuffer objs, JNI multiplies this by localSize */ - @Annotations.Unused @UsedByJNICode public int bytesPerLocalSize; + // @Annotations.Unused @UsedByJNICode public int bytesPerLocalWidth; /** * Only set for array objs, not used on JNI @@ -527,21 +530,25 @@ class KernelRunner{ * @param maxJTPLocalSize * @return */ - @Annotations.DocMe private native static synchronized long initJNI(Kernel _kernel, int _flags, int numProcessors, - int maxJTPLocalSize); + @Annotations.DocMe private native static synchronized long initJNI(Kernel _kernel, int _flags); private native long buildProgramJNI(long _jniContextHandle, String _source); private native int setArgsJNI(long _jniContextHandle, KernelArg[] _args, int argc); - private native int runKernelJNI(long _jniContextHandle, int _globalSize, int _localSize, boolean _needSync, - boolean useNullForLocalSize, int _passes); + private native int runKernelJNI(long _jniContextHandle, Range _range, boolean _needSync, int _passes); private native int disposeJNI(long _jniContextHandle); - private native int getLocalSizeJNI(long _jniContextHandle, int _globalSize, int localBytesPerLocalId); + private native String getExtensionsJNI(long _jniContextHandle); + + private native int getMaxWorkGroupSizeJNI(long _jniContextHandle); + + private native int getMaxWorkItemSizeJNI(long _jniContextHandle, int _index); + + private native int getMaxComputeUnitsJNI(long _jniContextHandle); - private native String getExtensions(long _jniContextHandle); + private native int getMaxWorkItemDimensionsJNI(long _jniContextHandle); private Set<String> capabilitiesSet; @@ -635,34 +642,6 @@ class KernelRunner{ return capabilitiesSet.contains(CL_KHR_GL_SHARING); } - private static int numCores = Runtime.getRuntime().availableProcessors(); - - private static int maxJTPLocalSize = Config.JTPLocalSizeMultiplier * numCores; - - static int getMaxJTPLocalSize() { - return maxJTPLocalSize; - } - - /** - * We need to match OpenCL's algorithm for localsize. - * - * @param _globalSize - * The globalsize requested by the user (via <code>Kernel.execute(globalSize)</code>) - * @return The value we use for JTP execution for localSize - */ - private static int getJTPLocalSizeForGlobalSize(int _globalSize) { - // iterate down until we find a localSize that divides _globalSize equally - for (int localSize = getMaxJTPLocalSize(); localSize > 0; localSize--) { - if (_globalSize % localSize == 0) { - if (logger.isLoggable(Level.FINE)) { - logger.fine("executeJava: picking localSize=" + localSize + " for globalSize=" + _globalSize); - } - return localSize; - } - } - return 0; - } - /** * Execute using a Java thread pool. Either because we were explicitly asked to do so, or because we 'fall back' after discovering an OpenCL issue. * @@ -672,108 +651,272 @@ class KernelRunner{ * The # of passes requested by the user (via <code>Kernel.execute(globalSize, passes)</code>). Note this is usually defaulted to 1 via <code>Kernel.execute(globalSize)</code>. * @return */ - private long executeJava(final int _globalSize, final int _passes) { + private long executeJava(final Range _range, final int _passes) { if (logger.isLoggable(Level.FINE)) { - logger.fine("executeJava: _globalSize=" + _globalSize); + logger.fine("executeJava: range = " + _range); } if (kernel.getExecutionMode().equals(EXECUTION_MODE.SEQ)) { - kernel.localBarrier = new CyclicBarrier(1); - - kernel.setSizes(_globalSize, 1); + /** + * SEQ mode is useful for testing trivial logic, but kernels which use SEQ mode cannot be used if the + * product of localSize(0..3) is >1. So we can use multi-dim ranges but only if the local size is 1 in all dimensions. + * + * As a result of this barrier is only ever 1 work item wide and probably should be turned into a no-op. + * + * So we need to check if the range is valid here. If not we have no choice but to punt. + */ + if (_range.getLocalSize(0) * _range.getLocalSize(1) * _range.getLocalSize(2) > 1) { + throw new IllegalStateException("Can't run range with group size >1 sequentially. Barriers would deadlock!"); + } - kernel.setNumGroups(_globalSize); - Kernel worker = (Kernel) kernel.clone(); - for (int passid = 0; passid < _passes; passid++) { - worker.setPassId(passid); - for (int id = 0; id < _globalSize; id++) { - worker.setGroupId(id); - worker.setGlobalId(id); - worker.setLocalId(0); - worker.run(); + Kernel kernelClone = (Kernel) kernel.clone(); + kernelClone.range = _range; + kernelClone.groupId[0] = 0; + kernelClone.groupId[1] = 0; + kernelClone.groupId[2] = 0; + kernelClone.localId[0] = 0; + kernelClone.localId[1] = 0; + kernelClone.localId[2] = 0; + kernelClone.localBarrier = new CyclicBarrier(1); + for (kernelClone.passId = 0; kernelClone.passId < _passes; kernelClone.passId++) { + + if (_range.getDims() == 1) { + for (int id = 0; id < _range.getGlobalSize(0); id++) { + kernelClone.globalId[0] = id; + kernelClone.run(); + } + } else if (_range.getDims() == 2) { + for (int x = 0; x < _range.getGlobalSize(0); x++) { + kernelClone.globalId[0] = x; + for (int y = 0; y < _range.getGlobalSize(1); y++) { + kernelClone.globalId[1] = y; + kernelClone.run(); + } + } + } else if (_range.getDims() == 3) { + for (int x = 0; x < _range.getGlobalSize(0); x++) { + kernelClone.globalId[0] = x; + for (int y = 0; y < _range.getGlobalSize(1); y++) { + kernelClone.globalId[1] = y; + for (int z = 0; z < _range.getGlobalSize(2); z++) { + kernelClone.globalId[2] = z; + kernelClone.run(); + } + kernelClone.run(); + } + } } } + } else { - // note uses of final so we can use in anonymous inner class - final int localSize = getJTPLocalSizeForGlobalSize(_globalSize); - // if (localSize == 0) return 0; // should never happen - final int numGroups = _globalSize / localSize; - - // compute numThreadSets by multiplying localSize until bigger than numCores - final int numThreadSets = localSize >= numCores ? 1 : (numCores + (localSize - 1)) / localSize; - final int numThreads = numThreadSets * localSize; - // when dividing to get groupsPerThreadSet, round up - final int groupsPerThreadSet = (numGroups + (numThreadSets - 1)) / numThreadSets; - if (logger.isLoggable(Level.FINE)) { - logger.fine("executeJava: localSize=" + localSize + ", numThreads=" + numThreads + ", numThreadSets=" + numThreadSets - + ", numGroups=" + numGroups); - } - Thread[] threads = new Thread[numThreads]; - // joinBarrier that says all threads are finished - final CyclicBarrier joinBarrier = new CyclicBarrier(numThreads + 1); - - // each threadSet shares a CyclicBarrier of size localSize - final CyclicBarrier localBarriers[] = new CyclicBarrier[numThreadSets]; - kernel.setSizes(_globalSize, localSize); - kernel.setNumGroups(numGroups); - for (int passid = 0; passid < _passes; passid++) { - kernel.setPassId(passid); - for (int thrSetId = 0; thrSetId < numThreadSets; thrSetId++) { - final int startGroupId = thrSetId * groupsPerThreadSet; - final int endGroupId = Math.min((thrSetId + 1) * groupsPerThreadSet, numGroups); - // System.out.println("thrSetId=" + thrSetId + " running groups from " + startGroupId + " thru " + (endGroupId-1)); - localBarriers[thrSetId] = new CyclicBarrier(localSize); - - // each threadSet has localSize threads - for (int lid = 0; lid < localSize; lid++) { // for each thread in threadSet - final int localId = lid; - final int threadId = thrSetId * localSize + localId; - final Kernel worker = (Kernel) kernel.clone(); - worker.setLocalId(localId); - worker.localBarrier = localBarriers[thrSetId]; // barrier that the kernel has access to - - threads[threadId] = new Thread(new Runnable(){ - @Override public void run() { - for (int groupId = startGroupId; groupId < endGroupId; groupId++) { - int globalId = (groupId * localSize) + localId; - worker.setGroupId(groupId); - worker.setGlobalId(globalId); - // System.out.println("running worker with gid=" + globalId + ", lid=" + localId - // + ", groupId=" + groupId + ", threadId=" + threadId); - worker.run(); - } - try { - joinBarrier.await(); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + final int threads = _range.getLocalSize(0) * _range.getLocalSize(1) * _range.getLocalSize(2); + final int globalGroups = _range.getNumGroups(0) * _range.getNumGroups(1) * _range.getNumGroups(2); + final Thread threadArray[] = new Thread[threads]; + /** + * This joinBarrier is the barrier that we provide for the kernel threads to rendezvous with the current dispatch thread. + * So this barrier is threadCount+1 wide (the +1 is for the dispatch thread) + */ + final CyclicBarrier joinBarrier = new CyclicBarrier(threads + 1); + + /** + * This localBarrier is only ever used by the kernels. If the kernel does not use the barrier the threads + * can get out of sync, we promised nothing in JTP mode. + * + * As with OpenCL all threads within a group must wait at the barrier or none. It is a user error (possible deadlock!) + * if the barrier is in a conditional that is only executed by some of the threads within a group. + * + * Kernel developer must understand this. + * + * This barrier is threadCount wide. We never hit the barrier from the dispatch thread. + */ + final CyclicBarrier localBarrier = new CyclicBarrier(threads); + + /** + * Note that we emulate OpenCL by creating one thread per localId (across the group). + * + * So threadCount == range.getLocalSize(0)*range.getLocalSize(1)*range.getLocalSize(2); + * + * For a 1D range of 12 groups of 4 we create 4 threads. One per localId(0). + * + * We also clone the kernel 4 times. One per thread. + * + * We create local barrier which has a width of 4 + * + * Thread-0 handles localId(0) (global 0,4,8) + * Thread-1 handles localId(1) (global 1,5,7) + * Thread-2 handles localId(2) (global 2,6,10) + * Thread-3 handles localId(3) (global 3,7,11) + * + * This allows all threads to synchronize using the local barrier. + * + * Initially the use of local buffers seems broken as the buffers appears to be per Kernel. + * Thankfully Kernel.clone() performs a shallow clone of all buffers (local and global) + * So each of the cloned kernels actually still reference the same underlying local/global buffers. + * + * If the kernel uses local buffers but does not use barriers then it is possible for different groups + * to see mutations from each other (unlike OpenCL), however if the kernel does not us barriers then it + * cannot assume any coherence in OpenCL mode either (the failure mode will be different but still wrong) + * + * So even JTP mode use of local buffers will need to use barriers. Not for the same reason as OpenCL but to keep groups in lockstep. + * + **/ + + for (int id = 0; id < threads; id++) { + final int threadId = id; + + /** + * We clone one kernel for each thread. + * + * They will all share references to the same range, localBarrier and global/local buffers because the clone is shallow. + * We need clones so that each thread can assign 'state' (localId/globalId/groupId) without worrying + * about other threads. + */ + final Kernel kernelClone = (Kernel) kernel.clone(); + kernelClone.range = _range; + kernelClone.localBarrier = localBarrier; + + threadArray[threadId] = new Thread(new Runnable(){ + @Override public void run() { + for (int globalGroupId = 0; globalGroupId < globalGroups; globalGroupId++) { + + if (_range.getDims() == 1) { + kernelClone.localId[0] = threadId % _range.getLocalSize(0); + kernelClone.globalId[0] = threadId + globalGroupId * threads; + kernelClone.groupId[0] = globalGroupId; + } else if (_range.getDims() == 2) { + + /** + * Consider a 12x4 grid of 4*2 local groups + * <pre> + * threads = 4*2 = 8 + * localWidth=4 + * localHeight=2 + * globalWidth=12 + * globalHeight=4 + * + * 00 01 02 03 | 04 05 06 07 | 08 09 10 11 + * 12 13 14 15 | 16 17 18 19 | 20 21 22 23 + * ------------+-------------+------------ + * 24 25 26 27 | 28 29 30 31 | 32 33 34 35 + * 36 37 38 39 | 40 41 42 43 | 44 45 46 47 + * + * 00 01 02 03 | 00 01 02 03 | 00 01 02 03 threadIds : [0..7]*6 + * 04 05 06 07 | 04 05 06 07 | 04 05 06 07 + * ------------+-------------+------------ + * 00 01 02 03 | 00 01 02 03 | 00 01 02 03 + * 04 05 06 07 | 04 05 06 07 | 04 05 06 07 + * + * 00 00 00 00 | 01 01 01 01 | 02 02 02 02 groupId[0] : 0..6 + * 00 00 00 00 | 01 01 01 01 | 02 02 02 02 + * ------------+-------------+------------ + * 00 00 00 00 | 01 01 01 01 | 02 02 02 02 + * 00 00 00 00 | 01 01 01 01 | 02 02 02 02 + * + * 00 00 00 00 | 00 00 00 00 | 00 00 00 00 groupId[1] : 0..6 + * 00 00 00 00 | 00 00 00 00 | 00 00 00 00 + * ------------+-------------+------------ + * 01 01 01 01 | 01 01 01 01 | 01 01 01 01 + * 01 01 01 01 | 01 01 01 01 | 01 01 01 01 + * + * 00 01 02 03 | 08 09 10 11 | 16 17 18 19 globalThreadIds == threadId + groupId * threads; + * 04 05 06 07 | 12 13 14 15 | 20 21 22 23 + * ------------+-------------+------------ + * 24 25 26 27 | 32[33]34 35 | 40 41 42 43 + * 28 29 30 31 | 36 37 38 39 | 44 45 46 47 + * + * 00 01 02 03 | 00 01 02 03 | 00 01 02 03 localX = threadId % localWidth; (for globalThreadId 33 = threadId = 01 : 01%4 =1) + * 00 01 02 03 | 00 01 02 03 | 00 01 02 03 + * ------------+-------------+------------ + * 00 01 02 03 | 00[01]02 03 | 00 01 02 03 + * 00 01 02 03 | 00 01 02 03 | 00 01 02 03 + * + * 00 00 00 00 | 00 00 00 00 | 00 00 00 00 localY = threadId /localWidth (for globalThreadId 33 = threadId = 01 : 01/4 =0) + * 01 01 01 01 | 01 01 01 01 | 01 01 01 01 + * ------------+-------------+------------ + * 00 00 00 00 | 00[00]00 00 | 00 00 00 00 + * 01 01 01 01 | 01 01 01 01 | 01 01 01 01 + * + * 00 01 02 03 | 04 05 06 07 | 08 09 10 11 globalX= + * 00 01 02 03 | 04 05 06 07 | 08 09 10 11 groupsPerLineWidth=globalWidth/localWidth (=12/4 =3) + * ------------+-------------+------------ groupInset =groupId%groupsPerLineWidth (=4%3 = 1) + * 00 01 02 03 | 04[05]06 07 | 08 09 10 11 + * 00 01 02 03 | 04 05 06 07 | 08 09 10 11 globalX = groupInset*localWidth+localX (= 1*4+1 = 5) + * + * 00 00 00 00 | 00 00 00 00 | 00 00 00 00 globalY + * 01 01 01 01 | 01 01 01 01 | 01 01 01 01 + * ------------+-------------+------------ + * 02 02 02 02 | 02[02]02 02 | 02 02 02 02 + * 03 03 03 03 | 03 03 03 03 | 03 03 03 03 + * + * </pre> + * Assume we are trying to locate the id's for #33 + * + */ + + kernelClone.localId[0] = threadId % _range.getLocalSize(0); // threadId % localWidth = (for 33 = 1 % 4 = 1) + kernelClone.localId[1] = threadId / _range.getLocalSize(0); // threadId / localWidth = (for 33 = 1 / 4 == 0) + + int groupInset = globalGroupId % _range.getNumGroups(0); // 4%3 = 1 + kernelClone.globalId[0] = groupInset * _range.getLocalSize(0) + kernelClone.localId[0]; // 1*4+1=5 + + int completeLines = (globalGroupId / _range.getNumGroups(0)) * _range.getLocalSize(1);// (4/3) * 2 + kernelClone.globalId[1] = completeLines + kernelClone.localId[1]; // 2+0 = 2 + kernelClone.groupId[0] = globalGroupId % _range.getNumGroups(0); + kernelClone.groupId[1] = globalGroupId / _range.getNumGroups(0); + } else if (_range.getDims() == 3) { + + //Same as 2D actually turns out that localId[0] is identical for all three dims so could be hoisted out of conditional code + + kernelClone.localId[0] = threadId % _range.getLocalSize(0); + + kernelClone.localId[1] = (threadId / _range.getLocalSize(0)) % _range.getLocalSize(1); + + // the thread id's span WxHxD so threadId/(WxH) should yield the local depth + kernelClone.localId[2] = threadId / (_range.getLocalSize(0) * _range.getLocalSize(1)); + + kernelClone.globalId[0] = (globalGroupId % _range.getNumGroups(0)) * _range.getLocalSize(0) + + kernelClone.localId[0]; + + kernelClone.globalId[1] = ((globalGroupId / _range.getNumGroups(0)) * _range.getLocalSize(1)) + % _range.getGlobalSize(1) + kernelClone.localId[1]; + + kernelClone.globalId[2] = (globalGroupId / (_range.getNumGroups(0) * _range.getNumGroups(1))) + * _range.getLocalSize(2) + kernelClone.localId[2]; + + kernelClone.groupId[0] = globalGroupId % _range.getNumGroups(0); + kernelClone.groupId[1] = (globalGroupId / _range.getNumGroups(0)) % _range.getNumGroups(1); + kernelClone.groupId[2] = globalGroupId / (_range.getNumGroups(0) * _range.getNumGroups(1)); } - }); - threads[threadId].start(); - } + kernelClone.run(); - // this is where the main thread waits on the join barrier - try { - joinBarrier.await(); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + } + await(joinBarrier); // This thread will rendezvous with dispatch thread here. This is effectively a join. } - } + }); + threadArray[threadId].setName("aparapi-" + threadId + "/" + threads); + threadArray[threadId].start(); + } + await(joinBarrier); // This dispatch thread waits for all worker threads here. + } // execution mode == JTP return 0; } + private static void await(CyclicBarrier _barrier) { + try { + _barrier.await(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (BrokenBarrierException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + private KernelArg[] args = null; private boolean usesOopConversion = false; @@ -1036,12 +1179,8 @@ class KernelRunner{ } } - // this routine now also finds out how many perLocalItem bytes are specified for this kernel - private int localBytesPerLocalId = 0; - private boolean updateKernelArrayRefs() throws AparapiException { boolean needsSync = false; - localBytesPerLocalId = 0; for (int i = 0; i < argc; i++) { KernelArg arg = args[i]; @@ -1089,37 +1228,59 @@ class KernelRunner{ // private int numAvailableProcessors = Runtime.getRuntime().availableProcessors(); - private Kernel executeOpenCL(final String _entrypointName, final int _globalSize, final int _passes) throws AparapiException { + private Kernel executeOpenCL(final String _entrypointName, final Range _range, final int _passes) throws AparapiException { + if (_range.getDims() > getMaxWorkItemDimensionsJNI(jniContextHandle)) { + throw new RangeException("Range dim size " + _range.getDims() + " > device " + + getMaxWorkItemDimensionsJNI(jniContextHandle)); + } + if (_range.getWorkGroupSize() > getMaxWorkGroupSizeJNI(jniContextHandle)) { + throw new RangeException("Range workgroup size " + _range.getWorkGroupSize() + " > device " + + getMaxWorkGroupSizeJNI(jniContextHandle)); + } + /* + if (_range.getGlobalSize(0) > getMaxWorkItemSizeJNI(jniContextHandle, 0)) { + throw new RangeException("Range globalsize 0 " + _range.getGlobalSize(0) + " > device " + + getMaxWorkItemSizeJNI(jniContextHandle, 0)); + } + if (_range.getDims() > 1) { + if (_range.getGlobalSize(1) > getMaxWorkItemSizeJNI(jniContextHandle, 1)) { + throw new RangeException("Range globalsize 1 " + _range.getGlobalSize(1) + " > device " + + getMaxWorkItemSizeJNI(jniContextHandle, 1)); + } + if (_range.getDims() > 2) { + if (_range.getGlobalSize(2) > getMaxWorkItemSizeJNI(jniContextHandle, 2)) { + throw new RangeException("Range globalsize 2 " + _range.getGlobalSize(2) + " > device " + + getMaxWorkItemSizeJNI(jniContextHandle, 2)); + } + } + } + */ + + if (logger.isLoggable(Level.FINE)) { + logger.fine("maxComputeUnits=" + this.getMaxComputeUnitsJNI(jniContextHandle)); + logger.fine("maxWorkGroupSize=" + this.getMaxWorkGroupSizeJNI(jniContextHandle)); + logger.fine("maxWorkItemDimensions=" + this.getMaxWorkItemDimensionsJNI(jniContextHandle)); + logger.fine("maxWorkItemSize(0)=" + getMaxWorkItemSizeJNI(jniContextHandle, 0)); + if (_range.getDims() > 1) { + logger.fine("maxWorkItemSize(1)=" + getMaxWorkItemSizeJNI(jniContextHandle, 1)); + if (_range.getDims() > 2) { + logger.fine("maxWorkItemSize(2)=" + getMaxWorkItemSizeJNI(jniContextHandle, 2)); + } + } + } // Read the array refs after kernel may have changed them // We need to do this as input to computing the localSize assert args != null : "args should not be null"; boolean needSync = updateKernelArrayRefs(); - - // note: the above will also recompute the value localBytesPerLocalId - - int localSize = getLocalSizeJNI(jniContextHandle, _globalSize, localBytesPerLocalId); - if (localSize == 0) { - // should fall back to java? - logger.warning("getLocalSizeJNI failed, reverting java"); - kernel.setFallbackExecutionMode(); - return execute(_entrypointName, _globalSize, _passes); - } - assert localSize <= _globalSize : "localSize = " + localSize; - - // Call back to kernel for last minute changes - kernel.setSizes(_globalSize, localSize); - if (needSync && logger.isLoggable(Level.FINE)) { logger.fine("Need to resync arrays on " + kernel.getClass().getName()); } - // native side will reallocate array buffers if necessary - if (runKernelJNI(jniContextHandle, _globalSize, localSize, needSync, kernel.useNullForLocalSize, _passes) != 0) { - //System.out.println("CL exec seems to have failed"); + if (runKernelJNI(jniContextHandle, _range, needSync, _passes) != 0) { logger.warning("### CL exec seems to have failed. Trying to revert to Java ###"); kernel.setFallbackExecutionMode(); - return execute(_entrypointName, _globalSize, _passes); + return execute(_entrypointName, _range, _passes); } if (usesOopConversion == true) { @@ -1127,42 +1288,41 @@ class KernelRunner{ } if (logger.isLoggable(Level.FINE)) { - logger.fine("executeOpenCL completed. _globalSize=" + _globalSize); + logger.fine("executeOpenCL completed. " + _range); } return kernel; } - synchronized Kernel execute(Kernel.Entry entry, final int _globalSize, final int _passes) { + synchronized Kernel execute(Kernel.Entry entry, final Range _range, final int _passes) { System.out.println("execute(Kernel.Entry, size) not implemented"); return (kernel); } - synchronized private Kernel fallBackAndExecute(String _entrypointName, final int _globalSize, final int _passes) { + synchronized private Kernel fallBackAndExecute(String _entrypointName, final Range _range, final int _passes) { kernel.setFallbackExecutionMode(); - return execute(_entrypointName, _globalSize, _passes); + return execute(_entrypointName, _range, _passes); } - synchronized private Kernel warnFallBackAndExecute(String _entrypointName, final int _globalSize, final int _passes, + synchronized private Kernel warnFallBackAndExecute(String _entrypointName, final Range _range, final int _passes, Exception _exception) { if (logger.isLoggable(Level.WARNING)) { logger.warning("Reverting to Java Thread Pool (JTP) for " + kernel.getClass() + ": " + _exception.getMessage()); _exception.printStackTrace(); } - return fallBackAndExecute(_entrypointName, _globalSize, _passes); + return fallBackAndExecute(_entrypointName, _range, _passes); } - synchronized private Kernel warnFallBackAndExecute(String _entrypointName, final int _globalSize, final int _passes, - String _excuse) { + synchronized private Kernel warnFallBackAndExecute(String _entrypointName, final Range _range, final int _passes, String _excuse) { logger.warning("Reverting to Java Thread Pool (JTP) for " + kernel.getClass() + ": " + _excuse); - return fallBackAndExecute(_entrypointName, _globalSize, _passes); + return fallBackAndExecute(_entrypointName, _range, _passes); } - synchronized Kernel execute(String _entrypointName, final int _globalSize, final int _passes) { + synchronized Kernel execute(String _entrypointName, final Range _range, final int _passes) { long executeStartTime = System.currentTimeMillis(); - if (_globalSize == 0) { - throw new IllegalStateException("global size can't be 0"); + if (_range == null) { + throw new IllegalStateException("range can't be null"); } if (kernel.getExecutionMode().isOpenCL()) { @@ -1173,7 +1333,7 @@ class KernelRunner{ entryPoint = classModel.getEntrypoint(_entrypointName, kernel); } catch (Exception exception) { - return warnFallBackAndExecute(_entrypointName, _globalSize, _passes, exception); + return warnFallBackAndExecute(_entrypointName, _range, _passes, exception); } if ((entryPoint != null) && !entryPoint.shouldFallback()) { @@ -1184,12 +1344,12 @@ class KernelRunner{ jniFlags |= (kernel.getExecutionMode().equals(EXECUTION_MODE.GPU) ? JNI_FLAG_USE_GPU : 0); // Init the device to check capabilities before emitting the // code that requires the capabilities. - jniContextHandle = initJNI(kernel, jniFlags, Runtime.getRuntime().availableProcessors(), getMaxJTPLocalSize()); + jniContextHandle = initJNI(kernel, jniFlags); if (jniContextHandle == 0) { - return warnFallBackAndExecute(_entrypointName, _globalSize, _passes, "initJNI failed to return a valid handle"); + return warnFallBackAndExecute(_entrypointName, _range, _passes, "initJNI failed to return a valid handle"); } - String extensions = getExtensions(jniContextHandle); + String extensions = getExtensionsJNI(jniContextHandle); capabilitiesSet = new HashSet<String>(); StringTokenizer strTok = new StringTokenizer(extensions); while (strTok.hasMoreTokens()) { @@ -1201,12 +1361,12 @@ class KernelRunner{ if (entryPoint.requiresDoublePragma() && !hasFP64Support()) { - return warnFallBackAndExecute(_entrypointName, _globalSize, _passes, "FP64 required but not supported"); + return warnFallBackAndExecute(_entrypointName, _range, _passes, "FP64 required but not supported"); } if (entryPoint.requiresByteAddressableStorePragma() && !hasByteAddressableStoreSupport()) { - return warnFallBackAndExecute(_entrypointName, _globalSize, _passes, + return warnFallBackAndExecute(_entrypointName, _range, _passes, "Byte addressable stores required but not supported"); } @@ -1215,24 +1375,16 @@ class KernelRunner{ if (entryPoint.requiresAtomic32Pragma() && !all32AtomicsAvailable) { - return warnFallBackAndExecute(_entrypointName, _globalSize, _passes, "32 bit Atomics required but not supported"); + return warnFallBackAndExecute(_entrypointName, _range, _passes, "32 bit Atomics required but not supported"); } - final StringBuilder openCLStringBuilder = new StringBuilder(); - KernelWriter openCLWriter = new KernelWriter(){ - @Override public void write(String _string) { - openCLStringBuilder.append(_string); - } - }; - - // Emit the OpenCL source into a string + String openCL = null; try { - openCLWriter.write(entryPoint); - + openCL = KernelWriter.writeToString(entryPoint); } catch (CodeGenException codeGenException) { - return warnFallBackAndExecute(_entrypointName, _globalSize, _passes, codeGenException); + return warnFallBackAndExecute(_entrypointName, _range, _passes, codeGenException); } - String openCL = openCLStringBuilder.toString(); + if (Config.enableShowGeneratedOpenCL) { System.out.println(openCL); } @@ -1241,9 +1393,8 @@ class KernelRunner{ } // Send the string to OpenCL to compile it - if (buildProgramJNI(jniContextHandle, openCLStringBuilder.toString()) == 0) { - - return warnFallBackAndExecute(_entrypointName, _globalSize, _passes, "OpenCL compile failed"); + if (buildProgramJNI(jniContextHandle, openCL) == 0) { + return warnFallBackAndExecute(_entrypointName, _range, _passes, "OpenCL compile failed"); } args = new KernelArg[entryPoint.getReferencedFields().size()]; @@ -1256,9 +1407,15 @@ class KernelRunner{ args[i].name = field.getName(); args[i].field = field; args[i].isStatic = (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC; - Class<?> type = field.getType(); if (type.isArray()) { + + if (field.getAnnotation(com.amd.aparapi.Kernel.Local.class) != null + || args[i].name.endsWith(Kernel.LOCAL_SUFFIX)) { + args[i].type |= ARG_LOCAL; + } else { + args[i].type |= ARG_GLOBAL; + } args[i].array = null; // will get updated in updateKernelArrayRefs args[i].type |= ARG_ARRAY; if (isExplicit()) { @@ -1269,7 +1426,7 @@ class KernelRunner{ args[i].type |= entryPoint.getArrayFieldAssignments().contains(field.getName()) ? (ARG_WRITE | ARG_READ) : 0; args[i].type |= entryPoint.getArrayFieldAccesses().contains(field.getName()) ? ARG_READ : 0; - args[i].type |= ARG_GLOBAL; + // args[i].type |= ARG_GLOBAL; args[i].type |= type.isAssignableFrom(float[].class) ? ARG_FLOAT : 0; args[i].type |= type.isAssignableFrom(int[].class) ? ARG_INT : 0; @@ -1351,26 +1508,26 @@ class KernelRunner{ conversionTime = System.currentTimeMillis() - executeStartTime; try { - executeOpenCL(_entrypointName, _globalSize, _passes); + executeOpenCL(_entrypointName, _range, _passes); } catch (AparapiException e) { - warnFallBackAndExecute(_entrypointName, _globalSize, _passes, e); + warnFallBackAndExecute(_entrypointName, _range, _passes, e); } } else { - warnFallBackAndExecute(_entrypointName, _globalSize, _passes, "failed to locate entrypoint"); + warnFallBackAndExecute(_entrypointName, _range, _passes, "failed to locate entrypoint"); } } else { try { - executeOpenCL(_entrypointName, _globalSize, _passes); + executeOpenCL(_entrypointName, _range, _passes); } catch (AparapiException e) { - warnFallBackAndExecute(_entrypointName, _globalSize, _passes, e); + warnFallBackAndExecute(_entrypointName, _range, _passes, e); } } } else { - executeJava(_globalSize, _passes); + executeJava(_range, _passes); } if (Config.enableExecutionModeReporting) { System.out.println(kernel.getClass().getCanonicalName() + ":" + kernel.getExecutionMode()); diff --git a/com.amd.aparapi/src/java/com/amd/aparapi/KernelWriter.java b/com.amd.aparapi/src/java/com/amd/aparapi/KernelWriter.java index 7a1886bb..a9eb0f28 100644 --- a/com.amd.aparapi/src/java/com/amd/aparapi/KernelWriter.java +++ b/com.amd.aparapi/src/java/com/amd/aparapi/KernelWriter.java @@ -43,10 +43,11 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import com.amd.aparapi.ClassModel.ClassModelField; import com.amd.aparapi.ClassModel.AttributePool.LocalVariableTableEntry; -import com.amd.aparapi.ClassModel.AttributePool.LocalVariableTableEntry.LocalVariableInfo; import com.amd.aparapi.ClassModel.AttributePool.RuntimeAnnotationsEntry; -import com.amd.aparapi.ClassModel.ClassModelField; +import com.amd.aparapi.ClassModel.AttributePool.LocalVariableTableEntry.LocalVariableInfo; +import com.amd.aparapi.ClassModel.AttributePool.RuntimeAnnotationsEntry.AnnotationInfo; import com.amd.aparapi.ClassModel.ConstantPool.FieldEntry; import com.amd.aparapi.ClassModel.ConstantPool.MethodEntry; import com.amd.aparapi.InstructionSet.AccessArrayElement; @@ -92,14 +93,47 @@ abstract class KernelWriter extends BlockWriter{ final static Map<String, String> javaToCLIdentifierMap = new HashMap<String, String>(); { + javaToCLIdentifierMap.put("getGlobalId()I", "get_global_id(0)"); + javaToCLIdentifierMap.put("getGlobalId(I)I", "get_global_id"); // no parenthesis if we are conveying args + javaToCLIdentifierMap.put("getGlobalX()I", "get_global_id(0)"); + javaToCLIdentifierMap.put("getGlobalY()I", "get_global_id(1)"); + javaToCLIdentifierMap.put("getGlobalZ()I", "get_global_id(2)"); + javaToCLIdentifierMap.put("getGlobalSize()I", "get_global_size(0)"); + javaToCLIdentifierMap.put("getGlobalSize(I)I", "get_global_size"); // no parenthesis if we are conveying args + javaToCLIdentifierMap.put("getGlobalWidth()I", "get_global_size(0)"); + javaToCLIdentifierMap.put("getGlobalHeight()I", "get_global_size(1)"); + javaToCLIdentifierMap.put("getGlobalDepth()I", "get_global_size(2)"); + javaToCLIdentifierMap.put("getLocalId()I", "get_local_id(0)"); + javaToCLIdentifierMap.put("getLocalId(I)I", "get_local_id"); // no parenthesis if we are conveying args + javaToCLIdentifierMap.put("getLocalX()I", "get_local_id(0)"); + javaToCLIdentifierMap.put("getLocalY()I", "get_local_id(1)"); + javaToCLIdentifierMap.put("getLocalZ()I", "get_local_id(2)"); + javaToCLIdentifierMap.put("getLocalSize()I", "get_local_size(0)"); + javaToCLIdentifierMap.put("getLocalSize(I)I", "get_local_size"); // no parenthesis if we are conveying args + javaToCLIdentifierMap.put("getLocalWidth()I", "get_local_size(0)"); + javaToCLIdentifierMap.put("getLocalHeight()I", "get_local_size(1)"); + javaToCLIdentifierMap.put("getLocalDepth()I", "get_local_size(2)"); + javaToCLIdentifierMap.put("getNumGroups()I", "get_num_groups(0)"); + javaToCLIdentifierMap.put("getNumGroups(I)I", "get_num_groups"); // no parenthesis if we are conveying args + javaToCLIdentifierMap.put("getNumGroupsX()I", "get_num_groups(0)"); + javaToCLIdentifierMap.put("getNumGroupsY()I", "get_num_groups(1)"); + javaToCLIdentifierMap.put("getNumGroupsZ()I", "get_num_groups(2)"); + javaToCLIdentifierMap.put("getGroupId()I", "get_group_id(0)"); + javaToCLIdentifierMap.put("getGroupId(I)I", "get_group_id"); // no parenthesis if we are conveying args + javaToCLIdentifierMap.put("getGroupX()I", "get_group_id(0)"); + javaToCLIdentifierMap.put("getGroupY()I", "get_group_id(1)"); + javaToCLIdentifierMap.put("getGroupZ()I", "get_group_id(2)"); + javaToCLIdentifierMap.put("getPassId()I", "get_pass_id(this)"); + javaToCLIdentifierMap.put("localBarrier()V", "barrier(CLK_LOCAL_MEM_FENCE)"); + javaToCLIdentifierMap.put("globalBarrier()V", "barrier(CLK_GLOBAL_MEM_FENCE)"); } @@ -160,7 +194,19 @@ abstract class KernelWriter extends BlockWriter{ if (barrierAndGetterMappings != null) { // this is one of the OpenCL barrier or size getter methods // write the mapping and exit - write(barrierAndGetterMappings); + if (argc > 0) { + write(barrierAndGetterMappings); + write("("); + for (int arg = 0; arg < argc; arg++) { + if ((arg != 0)) { + write(", "); + } + writeInstruction(_methodCall.getArg(arg)); + } + write(")"); + } else { + write(barrierAndGetterMappings); + } } else { String intrinsicMapping = Kernel.getMappedMethodName(_methodEntry); @@ -220,14 +266,17 @@ abstract class KernelWriter extends BlockWriter{ newLine(); } + public final static String __local = "__local"; + + public final static String __global = "__global"; + + public final static String LOCAL_ANNOTATION_NAME = "L" + Kernel.Local.class.getName().replace(".", "/") + ";"; + @Override void write(Entrypoint _entryPoint) throws CodeGenException { List<String> thisStruct = new ArrayList<String>(); List<String> argLines = new ArrayList<String>(); List<String> assigns = new ArrayList<String>(); - // hack - // for (java.lang.reflect.Field f:_entryPoint.getTheClass().getDeclaredFields()){ - entryPoint = _entryPoint; for (ClassModelField field : _entryPoint.getReferencedClassModelFields()) { @@ -239,13 +288,18 @@ abstract class KernelWriter extends BlockWriter{ String signature = field.getDescriptor(); boolean isPointer = false; + + // check the suffix + String type = field.getName().endsWith(Kernel.LOCAL_SUFFIX) ? __local : __global; RuntimeAnnotationsEntry visibleAnnotations = field.fieldAttributePool.getRuntimeVisibleAnnotationsEntry(); - String type = "__global"; if (visibleAnnotations != null) { - // for (AnnotationInfo ai : visibleAnnotations) { - // String typeDescriptor = ai.getTypeDescriptor(); - // } + for (AnnotationInfo ai : visibleAnnotations) { + String typeDescriptor = ai.getTypeDescriptor(); + if (typeDescriptor.equals(LOCAL_ANNOTATION_NAME)) { + type = __local; + } + } } if (signature.startsWith("[")) { diff --git a/com.amd.aparapi/src/java/com/amd/aparapi/MethodModel.java b/com.amd.aparapi/src/java/com/amd/aparapi/MethodModel.java index 4d590037..96106eea 100644 --- a/com.amd.aparapi/src/java/com/amd/aparapi/MethodModel.java +++ b/com.amd.aparapi/src/java/com/amd/aparapi/MethodModel.java @@ -117,8 +117,6 @@ class MethodModel{ private boolean methodIsGetter; private boolean methodIsSetter; - - // Only setters can use putfield private boolean usesPutfield; diff --git a/com.amd.aparapi/src/java/com/amd/aparapi/Range.java b/com.amd.aparapi/src/java/com/amd/aparapi/Range.java new file mode 100644 index 00000000..d0b59cad --- /dev/null +++ b/com.amd.aparapi/src/java/com/amd/aparapi/Range.java @@ -0,0 +1,392 @@ +package com.amd.aparapi; + +import java.util.Arrays; + +/** + * + * A representation of 1, 2 or 3 dimensional range of execution. + * + * This class uses factory methods to allow one, two or three dimensional ranges to be created. + * <br/> + * For a Kernel operating over the linear range 0..1024 without a specified groups size we would create a one dimensional <code>Range</code> using + * <blockquote><pre>Range.create(1024);</pre></blockquote> + * To request the same linear range but with a groupSize of 64 (range must be a multiple of group size!) we would use + * <blockquote><pre>Range.create(1024,64);</pre></blockquote> + * To request a two dimensional range over a grid (0..width)x(0..height) where width==512 and height=256 we would use + * <blockquote><pre> + * int width=512; + * int height=256; + * Range.create2D(width,height) + * </pre></blockquote> + * Again the above does not specify the group size. One will be chosen for you. If you want to specify the groupSize (say 16x8; 16 wide by 8 high) use + * <blockquote><pre> + * int width=512; + * int height=256; + * int groupWidth=16; + * int groupHeight=8; + * Range.create2D(width, height, groupWidth, groupHeight); + * </pre></blockquote> + * Finally we can request a three dimensional range using + * <blockquote><pre> + * int width=512; + * int height=256; + * int depth=8; + * Range.create3D(width, height, depth); + * </pre></blockquote> + * And can specify a group size using + * <blockquote><pre> + * int width=512; + * int height=256; + * int depth=8; + * int groupWidth=8; + * int groupHeight=4; + * int groupDepth=2 + * Range.create3D(width, height, depth, groupWidth, groupHeight, groupDepth); + * </pre></blockquote> + */ +public class Range{ + @KernelRunner.UsedByJNICode private int globalSize_0 = 1; + + @KernelRunner.UsedByJNICode private int localSize_0 = 1; + + @KernelRunner.UsedByJNICode private int globalSize_1 = 1; + + @KernelRunner.UsedByJNICode private int localSize_1 = 1; + + @KernelRunner.UsedByJNICode private int globalSize_2 = 1; + + @KernelRunner.UsedByJNICode private int localSize_2 = 1; + + @KernelRunner.UsedByJNICode private int dims; + + @KernelRunner.UsedByJNICode private boolean valid; + + @KernelRunner.UsedByJNICode private boolean localIsDerived = false; + + /** + * Get the localSize (of the group) given the requested dimension + * + * @param _dim 0=width, 1=height, 2=depth + * @return The size of the group give the requested dimension + */ + public int getLocalSize(int _dim) { + return (_dim == 0 ? localSize_0 : (_dim == 1 ? localSize_1 : localSize_2)); + } + + /** + * Get the globalSize (of the range) given the requested dimension + * + * @param _dim 0=width, 1=height, 2=depth + * @return The size of the group give the requested dimension + */ + public int getGlobalSize(int _dim) { + return (_dim == 0 ? globalSize_0 : (_dim == 1 ? globalSize_1 : globalSize_2)); + } + + private static final int THREADS_PER_CORE = 16; + + private static final int MAX_OPENCL_GROUP_SIZE = 256; + + private static final int MAX_GROUP_SIZE = Math.max(Runtime.getRuntime().availableProcessors() * THREADS_PER_CORE, + MAX_OPENCL_GROUP_SIZE); + + /** + * Create a one dimensional range <code>0.._globalWidth</code> which is processed in groups of size _localWidth. + * <br/> + * Note that for this range to be valid : </br> <strong><code>_globalWidth > 0 && _localWidth > 0 && _localWidth < MAX_GROUP_SIZE && _globalWidth % _localWidth==0</code></strong> + * + * @param _globalWidth the overall range we wish to process + * @param _localWidth the size of the group we wish to process. + * @return A new Range with the requested dimensions + */ + public static Range create(int _globalWidth, int _localWidth) { + Range range = new Range(); + range.dims = 1; + range.globalSize_0 = _globalWidth; + range.localSize_0 = _localWidth; + range.valid = range.localSize_0 > 0 && range.localSize_0 < MAX_GROUP_SIZE && range.globalSize_0 % range.localSize_0 == 0; + return (range); + } + + /** + * Determine the set of factors for a given value. + * @param _value The value we wish to factorize. + * @return and array of factors of _value + */ + + private static int[] getFactors(int _value) { + int factors[] = new int[MAX_GROUP_SIZE]; + int factorIdx = 0; + for (int possibleFactor = 1; possibleFactor <= MAX_GROUP_SIZE; possibleFactor++) { + if (_value % possibleFactor == 0) { + factors[factorIdx++] = possibleFactor; + } + } + return (Arrays.copyOf(factors, factorIdx)); + } + + /** + * Create a one dimensional range <code>0.._globalWidth</code> with an undefined group size. + * <br/> + * Note that for this range to be valid :- </br> <strong><code>_globalWidth > 0 </code></strong> + * <br/> + * The groupsize will be chosen such that _localWidth > 0 && _localWidth < MAX_GROUP_SIZE && _globalWidth % _localWidth==0 is true + * + * We extract the factors of _globalWidth and choose the highest value. + * + * @param _globalWidth the overall range we wish to process + * @return A new Range with the requested dimensions + */ + public static Range create(int _globalWidth) { + Range withoutLocal = create(_globalWidth, 1); + withoutLocal.localIsDerived = true; + int[] factors = getFactors(withoutLocal.globalSize_0); + + withoutLocal.localSize_0 = factors[factors.length - 1]; + + withoutLocal.valid = withoutLocal.localSize_0 > 0 && withoutLocal.localSize_0 < MAX_GROUP_SIZE + && withoutLocal.globalSize_0 % withoutLocal.localSize_0 == 0; + return (withoutLocal); + } + + /** + * Create a two dimensional range 0.._globalWidth x 0.._globalHeight using a group which is _localWidth x _localHeight in size. + * <br/> + * Note that for this range to be valid _globalWidth > 0 && _globalHeight >0 && _localWidth>0 && _localHeight>0 && _localWidth*_localHeight < MAX_GROUP_SIZE && _globalWidth%_localWidth==0 && _globalHeight%_localHeight==0. + * + * @param _globalWidth the overall range we wish to process + * @return + */ + public static Range create2D(int _globalWidth, int _globalHeight, int _localWidth, int _localHeight) { + Range range = new Range(); + range.dims = 2; + range.globalSize_0 = _globalWidth; + range.localSize_0 = _localWidth; + range.globalSize_1 = _globalHeight; + range.localSize_1 = _localHeight; + range.valid = range.localSize_0 > 0 && range.localSize_1 > 0 && range.localSize_0 * range.localSize_1 < MAX_GROUP_SIZE + && range.globalSize_0 % range.localSize_0 == 0 && range.globalSize_1 % range.localSize_1 == 0; + + return (range); + } + + /** + * Create a two dimensional range <code>0.._globalWidth * 0.._globalHeight</code> choosing suitable values for <code>localWidth</code> and <code>localHeight</code>. + * <p> + * Note that for this range to be valid <code>_globalWidth > 0 && _globalHeight >0 && _localWidth>0 && _localHeight>0 && _localWidth*_localHeight < MAX_GROUP_SIZE && _globalWidth%_localWidth==0 && _globalHeight%_localHeight==0</code>. + * + * <p> + * To determine suitable values for <code>_localWidth</code> and <code>_localHeight</code> we extract the factors for <code>_globalWidth</code> and <code>_globalHeight</code> and then + * find the largest product ( <code><= MAX_GROUP_SIZE</code>) with the lowest perimeter. + * + * <p> + * For example for <code>MAX_GROUP_SIZE</code> of 16 we favor 4x4 over 1x16. + * + * @param _globalWidth the overall range we wish to process + * @return + */ + public static Range create2D(int _globalWidth, int _globalHeight) { + Range withoutLocal = create2D(_globalWidth, _globalHeight, 1, 1); + withoutLocal.localIsDerived = true; + int[] widthFactors = getFactors(_globalWidth); + int[] heightFactors = getFactors(_globalHeight); + + withoutLocal.localSize_0 = 1; + withoutLocal.localSize_1 = 1; + int max = 1; + int perimeter = 0; + for (int w : widthFactors) { + for (int h : heightFactors) { + int size = w * h; + if (size > MAX_GROUP_SIZE) { + break; + } + + if (size > max) { + max = size; + perimeter = w + h; + withoutLocal.localSize_0 = w; + withoutLocal.localSize_1 = h; + } else if (size == max) { + int localPerimeter = w + h; + if (localPerimeter < perimeter) {// is this the shortest perimeter so far + perimeter = localPerimeter; + withoutLocal.localSize_0 = w; + withoutLocal.localSize_1 = h; + } + } + } + } + + withoutLocal.valid = withoutLocal.localSize_0 > 0 && withoutLocal.localSize_1 > 0 + && withoutLocal.localSize_0 * withoutLocal.localSize_1 < MAX_GROUP_SIZE + && withoutLocal.globalSize_0 % withoutLocal.localSize_0 == 0 + && withoutLocal.globalSize_1 % withoutLocal.localSize_1 == 0; + + return (withoutLocal); + } + + /** + * Create a two dimensional range <code>0.._globalWidth * 0.._globalHeight *0../_globalDepth</code> + * in groups defined by <code>localWidth</code> * <code>localHeight</code> * <code>localDepth</code>. + * <p> + * Note that for this range to be valid <code>_globalWidth > 0 && _globalHeight >0 _globalDepth >0 && _localWidth>0 && _localHeight>0 && _localDepth>0 && _localWidth*_localHeight*_localDepth < MAX_GROUP_SIZE && _globalWidth%_localWidth==0 && _globalHeight%_localHeight==0 && _globalDepth%_localDepth==0</code>. + * + * @param _globalWidth the width of the 3D grid we wish to process + * @param _globalHieght the height of the 3D grid we wish to process + * @param _globalDepth the depth of the 3D grid we wish to process + * @param _localWidth the width of the 3D group we wish to process + * @param _localHieght the height of the 3D group we wish to process + * @param _localDepth the depth of the 3D group we wish to process + * @return + */ + public static Range create3D(int _globalWidth, int _globalHeight, int _globalDepth, int _localWidth, int _localHeight, + int _localDepth) { + Range range = new Range(); + range.dims = 3; + range.globalSize_0 = _globalWidth; + range.localSize_0 = _localWidth; + range.globalSize_1 = _globalHeight; + range.localSize_1 = _localHeight; + range.globalSize_2 = _globalDepth; + range.localSize_2 = _localDepth; + range.valid = range.localSize_0 > 0 && range.localSize_1 > 0 && range.localSize_2 > 0 + && range.localSize_0 * range.localSize_1 * range.localSize_2 < MAX_GROUP_SIZE + && range.globalSize_0 % range.localSize_0 == 0 && range.globalSize_1 % range.localSize_1 == 0 + && range.globalSize_2 % range.localSize_2 == 0; + + return (range); + } + + /** + * Create a two dimensional range <code>0.._globalWidth * 0.._globalHeight *0../_globalDepth</code> + * choosing suitable values for <code>localWidth</code>, <code>localHeight</code> and <code>localDepth</code>. + * <p> + * Note that for this range to be valid <code>_globalWidth > 0 && _globalHeight >0 _globalDepth >0 && _localWidth>0 && _localHeight>0 && _localDepth>0 && _localWidth*_localHeight*_localDepth < MAX_GROUP_SIZE && _globalWidth%_localWidth==0 && _globalHeight%_localHeight==0 && _globalDepth%_localDepth==0</code>. + * + * <p> + * To determine suitable values for <code>_localWidth</code>,<code>_localHeight</code> and <code>_lodalDepth</code> we extract the factors for <code>_globalWidth</code>,<code>_globalHeight</code> and <code>_globalDepth</code> and then + * find the largest product ( <code><= MAX_GROUP_SIZE</code>) with the lowest perimeter. + * + * <p> + * For example for <code>MAX_GROUP_SIZE</code> of 64 we favor 4x4x4 over 1x16x16. + * + * @param _globalWidth the width of the 3D grid we wish to process + * @param _globalHieght the height of the 3D grid we wish to process + * @param _globalDepth the depth of the 3D grid we wish to process + * @return + */ + public static Range create3D(int _globalWidth, int _globalHeight, int _globalDepth) { + Range withoutLocal = create3D(_globalWidth, _globalHeight, _globalDepth, 1, 1, 1); + withoutLocal.localIsDerived = true; + int[] widthFactors = getFactors(_globalWidth); + int[] heightFactors = getFactors(_globalHeight); + int[] depthFactors = getFactors(_globalDepth); + + withoutLocal.localSize_0 = 1; + withoutLocal.localSize_1 = 1; + withoutLocal.localSize_2 = 1; + int max = 1; + int perimeter = 0; + for (int w : widthFactors) { + for (int h : heightFactors) { + for (int d : depthFactors) { + int size = w * h * d; + if (size > MAX_GROUP_SIZE) { + break; + } + if (size > max) { + max = size; + perimeter = w + h + d; + withoutLocal.localSize_0 = w; + withoutLocal.localSize_1 = h; + withoutLocal.localSize_2 = d; + } else if (size == max) { + int localPerimeter = w + h + d; + if (localPerimeter < perimeter) { // is this the shortest perimeter so far + perimeter = localPerimeter; + withoutLocal.localSize_0 = w; + withoutLocal.localSize_1 = h; + withoutLocal.localSize_2 = d; + } + } + } + } + } + + withoutLocal.valid = withoutLocal.localSize_0 > 0 && withoutLocal.localSize_1 > 0 && withoutLocal.localSize_2 > 0 + && withoutLocal.localSize_0 * withoutLocal.localSize_1 * withoutLocal.localSize_2 < MAX_GROUP_SIZE + && withoutLocal.globalSize_0 % withoutLocal.localSize_0 == 0 + && withoutLocal.globalSize_1 % withoutLocal.localSize_1 == 0 + && withoutLocal.globalSize_2 % withoutLocal.localSize_2 == 0; + + return (withoutLocal); + + } + + /** + * Get the number of dims for this Range. + * + * @return 0, 1 or 2 for one dimensional, two dimensional and three dimensional range respectively. + */ + public int getDims() { + return (dims); + } + + /** + * Override {@link #toString()} + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + + switch (dims) { + case 1: + + sb.append("global:" + globalSize_0 + " local:" + (localIsDerived ? "(derived)" : "") + localSize_0); + + break; + case 2: + sb.append("2D(global:" + globalSize_0 + "x" + globalSize_1 + " local:" + (localIsDerived ? "(derived)" : "") + + localSize_0 + "x" + localSize_1 + ")"); + break; + case 3: + sb.append("3D(global:" + globalSize_0 + "x" + globalSize_1 + "x" + globalSize_2 + " local:" + + (localIsDerived ? "(derived)" : "") + localSize_0 + "x" + localSize_1 + "x" + localSize_0 + ")"); + break; + + } + return (sb.toString()); + } + + /** + * Get the number of groups for the given dimension. + * + * <p> + * This will essentially return globalXXXX/localXXXX for the given dimension (width, height, depth) + * @param _dim The dim we are interested in 0, 1 or 2 + * @return the number of groups for the given dimension. + */ + + public int getNumGroups(int _dim) { + return (_dim == 0 ? (globalSize_0 / localSize_0) : (_dim == 1 ? (globalSize_1 / localSize_1) : (globalSize_2 / localSize_2))); + } + + /** + * + * @return The product of all valid localSize dimensions + */ + public int getWorkGroupSize() { + return localSize_0 * localSize_1 * localSize_2; + } + + /** + * Determine whether this Range is usable. + * + * @return true if this Range is usable/valid. + */ + + public boolean isValid() { + return (valid); + } + +} diff --git a/com.amd.aparapi/src/java/com/amd/aparapi/RangeException.java b/com.amd.aparapi/src/java/com/amd/aparapi/RangeException.java new file mode 100644 index 00000000..b3998f84 --- /dev/null +++ b/com.amd.aparapi/src/java/com/amd/aparapi/RangeException.java @@ -0,0 +1,46 @@ +/* +Copyright (c) 2010-2011, Advanced Micro Devices, Inc. +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 the copyright holder 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 HOLDER 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. + +If you use the software (in whole or in part), you shall adhere to all applicable U.S., European, and other export +laws, including but not limited to the U.S. Export Administration Regulations ("EAR"), (15 C.F.R. Sections 730 through +774), and E.U. Council Regulation (EC) No 1334/2000 of 22 June 2000. Further, pursuant to Section 740.6 of the EAR, +you hereby certify that, except pursuant to a license granted by the United States Department of Commerce Bureau of +Industry and Security or as otherwise permitted pursuant to a License Exception under the U.S. Export Administration +Regulations ("EAR"), you will not (1) export, re-export or release to a national of a country in Country Groups D:1, +E:1 or E:2 any restricted technology, software, or source code you receive hereunder, or (2) export to Country Groups +D:1, E:1 or E:2 the direct product of such technology or software, if such foreign produced direct product is subject +to national security controls as identified on the Commerce Control List (currently found in Supplement 1 to Part 774 +of EAR). For the most current Country Group listings, or for additional information about the EAR or your obligations +under those regulations, please refer to the U.S. Bureau of Industry and Security's website at http://www.bis.doc.gov/. + +*/ +package com.amd.aparapi; + +@SuppressWarnings("serial") class RangeException extends AparapiException{ + + RangeException(String msg) { + super(msg); + } + +} diff --git a/examples/effects/src/com/amd/aparapi/examples/effects/Main.java b/examples/effects/src/com/amd/aparapi/examples/effects/Main.java index dab3078f..5543d94c 100644 --- a/examples/effects/src/com/amd/aparapi/examples/effects/Main.java +++ b/examples/effects/src/com/amd/aparapi/examples/effects/Main.java @@ -53,6 +53,7 @@ import javax.swing.JComponent; import javax.swing.JFrame; import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; /** * An example Aparapi application which tracks the mouse and updates the color pallete of the window based on the distance from the mouse pointer. @@ -161,6 +162,8 @@ public class Main{ /** Height of Mandelbrot view. */ final int height = 1024; + final Range range = Range.create2D(width, height); + /** The size of the pallette of pixel colors we choose from. */ final int palletteSize = 128; @@ -234,7 +237,7 @@ public class Main{ int trailLastUpdatedPosition = 0; - kernel.execute(width * height); + kernel.execute(range); System.arraycopy(rgb, 0, imageRgb, 0, rgb.length); viewer.repaint(); @@ -268,7 +271,7 @@ public class Main{ trailLastUpdatedPosition++; /** execute the kernel which calculates new pixel values **/ - kernel.execute(width * height); + kernel.execute(range); /** copy the rgb values to the imageRgb buffer **/ System.arraycopy(rgb, 0, imageRgb, 0, rgb.length); diff --git a/examples/nbody/local.bat b/examples/nbody/local.bat new file mode 100644 index 00000000..9fe4eb04 --- /dev/null +++ b/examples/nbody/local.bat @@ -0,0 +1,14 @@ +@echo off + +java ^ + -Djava.library.path=..\..\com.amd.aparapi.jni;jogamp ^ + -Dcom.amd.aparapi.executionMode=%1 ^ + -Dcom.amd.aparapi.enableShowGeneratedOpenCL=true ^ + -Dcom.amd.aparapi.enableVerboseJNI=false ^ + -Dbodies=%2 ^ + -Dheight=600 ^ + -Dwidth=600 ^ + -classpath jogamp\gluegen-rt.jar;jogamp\jogl.all.jar;..\..\com.amd.aparapi\aparapi.jar;nbody.jar ^ + com.amd.aparapi.examples.nbody.Local + + diff --git a/examples/nbody/src/com/amd/aparapi/examples/nbody/Local.java b/examples/nbody/src/com/amd/aparapi/examples/nbody/Local.java new file mode 100644 index 00000000..7fd272e2 --- /dev/null +++ b/examples/nbody/src/com/amd/aparapi/examples/nbody/Local.java @@ -0,0 +1,356 @@ +/* +Copyright (c) 2010-2011, Advanced Micro Devices, Inc. +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 the copyright holder 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 HOLDER 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. + +If you use the software (in whole or in part), you shall adhere to all applicable U.S., European, and other export +laws, including but not limited to the U.S. Export Administration Regulations ("EAR"), (15 C.F.R. Sections 730 through +774), and E.U. Council Regulation (EC) No 1334/2000 of 22 June 2000. Further, pursuant to Section 740.6 of the EAR, +you hereby certify that, except pursuant to a license granted by the United States Department of Commerce Bureau of +Industry and Security or as otherwise permitted pursuant to a License Exception under the U.S. Export Administration +Regulations ("EAR"), you will not (1) export, re-export or release to a national of a country in Country Groups D:1, +E:1 or E:2 any restricted technology, software, or source code you receive hereunder, or (2) export to Country Groups +D:1, E:1 or E:2 the direct product of such technology or software, if such foreign produced direct product is subject +to national security controls as identified on the Commerce Control List (currently found in Supplement 1 to Part 774 +of EAR). For the most current Country Group listings, or for additional information about the EAR or your obligations +under those regulations, please refer to the U.S. Bureau of Industry and Security's website at http://www.bis.doc.gov/. + +*/ +package com.amd.aparapi.examples.nbody; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.IOException; +import java.io.InputStream; + +import javax.media.opengl.GL; +import javax.media.opengl.GL2; +import javax.media.opengl.GLAutoDrawable; +import javax.media.opengl.GLCapabilities; +import javax.media.opengl.GLEventListener; +import javax.media.opengl.GLException; +import javax.media.opengl.awt.GLCanvas; +import javax.media.opengl.fixedfunc.GLLightingFunc; +import javax.media.opengl.glu.GLU; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.WindowConstants; + +import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; +import com.jogamp.opengl.util.FPSAnimator; +import com.jogamp.opengl.util.texture.Texture; +import com.jogamp.opengl.util.texture.TextureIO; + +/** + * An NBody clone which uses local memory to cache NBody positions for execution. + * + * http://www.browndeertechnology.com/docs/BDT_OpenCL_Tutorial_NBody-rev3.html + * + * @see com.amd.aparapi.examples.nbody.Main + * + * @author gfrost + * + */ +public class Local{ + + public static class NBodyKernel extends Kernel{ + protected final float delT = .005f; + + protected final float espSqr = 1.0f; + + protected final float mass = 5f; + + private final Range range; + + private final float[] xyz; // positions xy and z of bodies + + private final float[] vxyz; // velocity component of x,y and z of bodies + + @Local private final float[] localStuff; // local memory + + /** + * Constructor initializes xyz and vxyz arrays. + * @param _bodies + */ + public NBodyKernel(Range _range) { + range = _range; + localStuff = new float[range.getLocalSize(0) * 3]; + + xyz = new float[range.getGlobalSize(0) * 3]; + vxyz = new float[range.getGlobalSize(0) * 3]; + float maxDist = 20f; + for (int body = 0; body < range.getGlobalSize(0) * 3; body += 3) { + float theta = (float) (Math.random() * Math.PI * 2); + float phi = (float) (Math.random() * Math.PI * 2); + float radius = (float) (Math.random() * maxDist); + + // get the 3D dimensional coordinates + xyz[body + 0] = (float) (radius * Math.cos(theta) * Math.sin(phi)); + xyz[body + 1] = (float) (radius * Math.sin(theta) * Math.sin(phi)); + xyz[body + 2] = (float) (radius * Math.cos(phi)); + + // divide into two 'spheres of bodies' by adjusting x + if (body % 2 == 0) { + xyz[body + 0] += maxDist * 1.5; + } else { + xyz[body + 0] -= maxDist * 1.5; + } + } + setExplicit(true); + } + + /** + * Here is the kernel entrypoint. Here is where we calculate the position of each body + */ + @Override public void run() { + + int globalId = getGlobalId(0) * 3; + + float accx = 0.f; + float accy = 0.f; + float accz = 0.f; + float myPosx = xyz[globalId + 0]; + float myPosy = xyz[globalId + 1]; + float myPosz = xyz[globalId + 2]; + + for (int tile = 0; tile < getGlobalSize(0) / getLocalSize(0); tile++) { + // load one tile into local memory + int gidx = (tile * getLocalSize(0) + getLocalId()) * 3; + int lidx = getLocalId(0) * 3; + localStuff[lidx + 0] = xyz[gidx + 0]; + localStuff[lidx + 1] = xyz[gidx + 1]; + localStuff[lidx + 2] = xyz[gidx + 2]; + // Synchronize to make sure data is available for processing + localBarrier(); + + for (int i = 0; i < getLocalSize() * 3; i += 3) { + float dx = localStuff[i + 0] - myPosx; + float dy = localStuff[i + 1] - myPosy; + float dz = localStuff[i + 2] - myPosz; + float invDist = rsqrt((dx * dx) + (dy * dy) + (dz * dz) + espSqr); + float s = mass * invDist * invDist * invDist; + accx = accx + s * dx; + accy = accy + s * dy; + accz = accz + s * dz; + } + localBarrier(); + } + accx = accx * delT; + accy = accy * delT; + accz = accz * delT; + xyz[globalId + 0] = myPosx + vxyz[globalId + 0] * delT + accx * .5f * delT; + xyz[globalId + 1] = myPosy + vxyz[globalId + 1] * delT + accy * .5f * delT; + xyz[globalId + 2] = myPosz + vxyz[globalId + 2] * delT + accz * .5f * delT; + + vxyz[globalId + 0] = vxyz[globalId + 0] + accx; + vxyz[globalId + 1] = vxyz[globalId + 1] + accy; + vxyz[globalId + 2] = vxyz[globalId + 2] + accz; + } + + /** + * Render all particles to the OpenGL context + * @param gl + */ + + protected void render(GL2 gl) { + gl.glBegin(GL2.GL_QUADS); + + for (int i = 0; i < range.getGlobalSize(0) * 3; i += 3) { + gl.glTexCoord2f(0, 1); + gl.glVertex3f(xyz[i + 0], xyz[i + 1] + 1, xyz[i + 2]); + gl.glTexCoord2f(0, 0); + gl.glVertex3f(xyz[i + 0], xyz[i + 1], xyz[i + 2]); + gl.glTexCoord2f(1, 0); + gl.glVertex3f(xyz[i + 0] + 1, xyz[i + 1], xyz[i + 2]); + gl.glTexCoord2f(1, 1); + gl.glVertex3f(xyz[i + 0] + 1, xyz[i + 1] + 1, xyz[i + 2]); + } + gl.glEnd(); + } + + } + + public static int width; + + public static int height; + + public static boolean running; + + public static void main(String _args[]) { + + final NBodyKernel kernel = new NBodyKernel(Range.create(Integer.getInteger("bodies", 8192), 256)); + + JFrame frame = new JFrame("NBody"); + + JPanel panel = new JPanel(new BorderLayout()); + JPanel controlPanel = new JPanel(new FlowLayout()); + panel.add(controlPanel, BorderLayout.SOUTH); + + final JButton startButton = new JButton("Start"); + + startButton.addActionListener(new ActionListener(){ + @Override public void actionPerformed(ActionEvent e) { + running = true; + startButton.setEnabled(false); + } + }); + controlPanel.add(startButton); + controlPanel.add(new JLabel(kernel.getExecutionMode().toString())); + + controlPanel.add(new JLabel(" Particles")); + controlPanel.add(new JTextField("" + kernel.range.getGlobalSize(0), 5)); + + controlPanel.add(new JLabel("FPS")); + final JTextField framesPerSecondTextField = new JTextField("0", 5); + + controlPanel.add(framesPerSecondTextField); + controlPanel.add(new JLabel("Score(")); + JLabel miniLabel = new JLabel("<html><small>calcs</small><hr/><small>µsec</small></html>"); + + controlPanel.add(miniLabel); + controlPanel.add(new JLabel(")")); + + final JTextField positionUpdatesPerMicroSecondTextField = new JTextField("0", 5); + + controlPanel.add(positionUpdatesPerMicroSecondTextField); + GLCapabilities caps = new GLCapabilities(null); + caps.setDoubleBuffered(true); + caps.setHardwareAccelerated(true); + final GLCanvas canvas = new GLCanvas(caps); + Dimension dimension = new Dimension(Integer.getInteger("width", 742), Integer.getInteger("height", 742)); + canvas.setPreferredSize(dimension); + + canvas.addGLEventListener(new GLEventListener(){ + private double ratio; + + private final float xeye = 0f; + + private final float yeye = 0f; + + private final float zeye = 100f; + + private final float xat = 0f; + + private final float yat = 0f; + + private final float zat = 0f; + + public final float zoomFactor = 1.0f; + + private int frames; + + private long last = System.currentTimeMillis(); + + @Override public void dispose(GLAutoDrawable drawable) { + + } + + @Override public void display(GLAutoDrawable drawable) { + + GL2 gl = drawable.getGL().getGL2(); + + gl.glLoadIdentity(); + gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); + gl.glColor3f(1f, 1f, 1f); + + GLU glu = new GLU(); + glu.gluPerspective(45f, ratio, 0f, 1000f); + + glu.gluLookAt(xeye, yeye, zeye * zoomFactor, xat, yat, zat, 0f, 1f, 0f); + if (running) { + kernel.execute(kernel.range); + if (kernel.isExplicit()) { + kernel.get(kernel.xyz); + } + } + kernel.render(gl); + + long now = System.currentTimeMillis(); + long time = now - last; + frames++; + + if (time > 1000) { // We update the frames/sec every second + if (running) { + float framesPerSecond = (frames * 1000.0f) / time; + int updatesPerMicroSecond = (int) ((framesPerSecond * kernel.range.getGlobalSize(0) * kernel.range + .getGlobalSize(0)) / 1000000); + framesPerSecondTextField.setText(String.format("%5.2f", framesPerSecond)); + positionUpdatesPerMicroSecondTextField.setText(String.format("%4d", updatesPerMicroSecond)); + } + frames = 0; + last = now; + } + gl.glFlush(); + + } + + @Override public void init(GLAutoDrawable drawable) { + final GL2 gl = drawable.getGL().getGL2(); + + gl.glShadeModel(GLLightingFunc.GL_SMOOTH); + gl.glEnable(GL.GL_BLEND); + gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE); + try { + InputStream textureStream = Local.class.getResourceAsStream("particle.jpg"); + Texture texture = TextureIO.newTexture(textureStream, false, null); + texture.enable(gl); + } catch (IOException e) { + e.printStackTrace(); + } catch (GLException e) { + e.printStackTrace(); + } + + } + + @Override public void reshape(GLAutoDrawable drawable, int x, int y, int _width, int _height) { + width = _width; + height = _height; + + GL2 gl = drawable.getGL().getGL2(); + gl.glViewport(0, 0, width, height); + + ratio = (double) width / (double) height; + + } + + }); + + panel.add(canvas, BorderLayout.CENTER); + frame.getContentPane().add(panel, BorderLayout.CENTER); + + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + frame.pack(); + frame.setVisible(true); + + FPSAnimator animator = new FPSAnimator(canvas, 100); + animator.start(); + + } + +} diff --git a/examples/nbody/src/com/amd/aparapi/examples/nbody/Main.java b/examples/nbody/src/com/amd/aparapi/examples/nbody/Main.java index ae92a90a..016a7084 100644 --- a/examples/nbody/src/com/amd/aparapi/examples/nbody/Main.java +++ b/examples/nbody/src/com/amd/aparapi/examples/nbody/Main.java @@ -62,10 +62,23 @@ import javax.swing.JTextField; import javax.swing.WindowConstants; import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; import com.jogamp.opengl.util.FPSAnimator; import com.jogamp.opengl.util.texture.Texture; import com.jogamp.opengl.util.texture.TextureIO; +/** + * NBody implementing demonstrating Aparapi kernels. + * + * For a description of the NBody problem. + * @see http://en.wikipedia.org/wiki/N-body_problem + * + * We use JOGL to render the bodies. + * @see http://jogamp.org/jogl/www/ + * + * @author gfrost + * + */ public class Main{ public static class NBodyKernel extends Kernel{ @@ -75,7 +88,7 @@ public class Main{ protected final float mass = 5f; - private final int bodies; + private final Range range; private final float[] xyz; // positions xy and z of bodies @@ -85,27 +98,32 @@ public class Main{ * Constructor initializes xyz and vxyz arrays. * @param _bodies */ - public NBodyKernel(int _bodies) { - bodies = _bodies; - xyz = new float[bodies * 3]; - vxyz = new float[bodies * 3]; + public NBodyKernel(Range _range) { + range = _range; + // range = Range.create(bodies); + xyz = new float[range.getGlobalSize(0) * 3]; + vxyz = new float[range.getGlobalSize(0) * 3]; float maxDist = 20f; - for (int body = 0; body < bodies * 3; body += 3) { - // If I could remmember some basic algebra I guess I could avoid this loop ;) - // just ensures that the x,y,z is within maxdist radius of origin - do { - xyz[body + 0] = (float) (Math.random() * 2 * maxDist) - maxDist; //x - xyz[body + 1] = (float) (Math.random() * 2 * maxDist) - maxDist; //y - xyz[body + 2] = (float) (Math.random() * 2 * maxDist) - maxDist; //z - } while (xyz[body + 0] * xyz[body + 0] + xyz[body + 1] * xyz[body + 1] + xyz[body + 2] * xyz[body + 2] > maxDist - * maxDist); - // divide into two 'sphere of bodies' by adjusting x + for (int body = 0; body < range.getGlobalSize(0) * 3; body += 3) { + + float theta = (float) (Math.random() * Math.PI * 2); + float phi = (float) (Math.random() * Math.PI * 2); + float radius = (float) (Math.random() * maxDist); + + // get the 3D dimensional coordinates + xyz[body + 0] = (float) (radius * Math.cos(theta) * Math.sin(phi)); + xyz[body + 1] = (float) (radius * Math.sin(theta) * Math.sin(phi)); + xyz[body + 2] = (float) (radius * Math.cos(phi)); + + // divide into two 'spheres of bodies' by adjusting x + if (body % 2 == 0) { xyz[body + 0] += maxDist * 1.5; } else { xyz[body + 0] -= maxDist * 1.5; } } + setExplicit(true); } /** @@ -113,7 +131,7 @@ public class Main{ */ @Override public void run() { int body = getGlobalId(); - int count = bodies * 3; + int count = getGlobalSize(0) * 3; int globalId = body * 3; float accx = 0.f; @@ -153,7 +171,7 @@ public class Main{ protected void render(GL2 gl) { gl.glBegin(GL2.GL_QUADS); - for (int i = 0; i < bodies * 3; i += 3) { + for (int i = 0; i < range.getGlobalSize(0) * 3; i += 3) { gl.glTexCoord2f(0, 1); gl.glVertex3f(xyz[i + 0], xyz[i + 1] + 1, xyz[i + 2]); gl.glTexCoord2f(0, 0); @@ -176,7 +194,7 @@ public class Main{ public static void main(String _args[]) { - final NBodyKernel kernel = new NBodyKernel(Integer.getInteger("bodies", 8192)); + final NBodyKernel kernel = new NBodyKernel(Range.create(Integer.getInteger("bodies", 8192))); JFrame frame = new JFrame("NBody"); @@ -196,7 +214,7 @@ public class Main{ controlPanel.add(new JLabel(kernel.getExecutionMode().toString())); controlPanel.add(new JLabel(" Particles")); - controlPanel.add(new JTextField("" + kernel.bodies, 5)); + controlPanel.add(new JTextField("" + kernel.range.getGlobalSize(0), 5)); controlPanel.add(new JLabel("FPS")); final JTextField framesPerSecondTextField = new JTextField("0", 5); @@ -256,7 +274,10 @@ public class Main{ glu.gluLookAt(xeye, yeye, zeye * zoomFactor, xat, yat, zat, 0f, 1f, 0f); if (running) { - kernel.execute(kernel.bodies); + kernel.execute(kernel.range); + if (kernel.isExplicit()) { + kernel.get(kernel.xyz); + } } kernel.render(gl); @@ -267,7 +288,8 @@ public class Main{ if (time > 1000) { // We update the frames/sec every second if (running) { float framesPerSecond = (frames * 1000.0f) / time; - int updatesPerMicroSecond = (int) ((framesPerSecond * kernel.bodies * kernel.bodies) / 1000000); + int updatesPerMicroSecond = (int) ((framesPerSecond * kernel.range.getGlobalSize(0) * kernel.range + .getGlobalSize(0)) / 1000000); framesPerSecondTextField.setText(String.format("%5.2f", framesPerSecond)); positionUpdatesPerMicroSecondTextField.setText(String.format("%4d", updatesPerMicroSecond)); } @@ -316,7 +338,7 @@ public class Main{ frame.pack(); frame.setVisible(true); - FPSAnimator animator = new FPSAnimator(canvas, 200); + FPSAnimator animator = new FPSAnimator(canvas, 100); animator.start(); } diff --git a/samples/blackscholes/.classpath b/samples/blackscholes/.classpath new file mode 100644 index 00000000..2b3d4294 --- /dev/null +++ b/samples/blackscholes/.classpath @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/> + <classpathentry combineaccessrules="false" kind="src" path="/com.amd.aparapi"/> + <classpathentry kind="output" path="classes"/> +</classpath> diff --git a/samples/blackscholes/.project b/samples/blackscholes/.project new file mode 100644 index 00000000..eb5be55b --- /dev/null +++ b/samples/blackscholes/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>blackscholes</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/samples/blackscholes/src/com/amd/aparapi/samples/blackscholes/Main.java b/samples/blackscholes/src/com/amd/aparapi/samples/blackscholes/Main.java index a80ddd65..60c31142 100644 --- a/samples/blackscholes/src/com/amd/aparapi/samples/blackscholes/Main.java +++ b/samples/blackscholes/src/com/amd/aparapi/samples/blackscholes/Main.java @@ -38,6 +38,7 @@ under those regulations, please refer to the U.S. Bureau of Industry and Securit package com.amd.aparapi.samples.blackscholes; import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; public class Main{ @@ -181,9 +182,10 @@ public class Main{ public static void main(String[] _args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { int size = Integer.getInteger("size", 512); - int iterations = Integer.getInteger("iterations", 5); - System.out.println("size ="+size); - System.out.println("iterations ="+iterations); + Range range = Range.create(size); + int iterations = Integer.getInteger("iterations", 5); + System.out.println("size =" + size); + System.out.println("iterations =" + iterations); BlackScholesKernel kernel = new BlackScholesKernel(size); long totalExecTime = 0; @@ -193,7 +195,7 @@ public class Main{ iterExecTime = kernel.execute(size).getExecutionTime(); totalExecTime += iterExecTime; }*/ - kernel.execute(size, iterations); + kernel.execute(range, iterations); System.out.println("Average execution time " + kernel.getAccumulatedExecutionTime() / iterations); kernel.showResults(10); diff --git a/samples/convolution/.classpath b/samples/convolution/.classpath new file mode 100644 index 00000000..2b3d4294 --- /dev/null +++ b/samples/convolution/.classpath @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/> + <classpathentry combineaccessrules="false" kind="src" path="/com.amd.aparapi"/> + <classpathentry kind="output" path="classes"/> +</classpath> diff --git a/samples/convolution/.project b/samples/convolution/.project new file mode 100644 index 00000000..a304e12f --- /dev/null +++ b/samples/convolution/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>convolution</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/samples/convolution/build.xml b/samples/convolution/build.xml new file mode 100644 index 00000000..90979e33 --- /dev/null +++ b/samples/convolution/build.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> + +<project name="convolution" default="build" basedir="."> + <target name="build" depends="clean"> + <mkdir dir="classes"/> + <javac srcdir="src" destdir="classes" debug="on" includeantruntime="false" > + <classpath> + <pathelement path="../../com.amd.aparapi/aparapi.jar"/> + </classpath> + </javac> + <jar jarfile="${ant.project.name}.jar" basedir="classes"/> + </target> + + <target name="clean"> + <delete dir="classes"/> + <delete file="${ant.project.name}.jar"/> + </target> + + +</project> diff --git a/samples/convolution/conv.bat b/samples/convolution/conv.bat new file mode 100644 index 00000000..ac0c4aab --- /dev/null +++ b/samples/convolution/conv.bat @@ -0,0 +1,6 @@ +java ^ + -Djava.library.path=../../com.amd.aparapi.jni ^ + -Dcom.amd.aparapi.executionMode=%1 ^ + -classpath ../../com.amd.aparapi/aparapi.jar;convolution.jar ^ + com.amd.aparapi.sample.convolution.Main + diff --git a/samples/convolution/src/com/amd/aparapi/sample/convolution/Main.java b/samples/convolution/src/com/amd/aparapi/sample/convolution/Main.java new file mode 100644 index 00000000..01964181 --- /dev/null +++ b/samples/convolution/src/com/amd/aparapi/sample/convolution/Main.java @@ -0,0 +1,234 @@ +/* +Copyright (c) 2010-2011, Advanced Micro Devices, Inc. +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 the copyright holder 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 HOLDER 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. + +If you use the software (in whole or in part), you shall adhere to all applicable U.S., European, and other export +laws, including but not limited to the U.S. Export Administration Regulations ("EAR"), (15 C.F.R. Sections 730 through +774), and E.U. Council Regulation (EC) No 1334/2000 of 22 June 2000. Further, pursuant to Section 740.6 of the EAR, +you hereby certify that, except pursuant to a license granted by the United States Department of Commerce Bureau of +Industry and Security or as otherwise permitted pursuant to a License Exception under the U.S. Export Administration +Regulations ("EAR"), you will not (1) export, re-export or release to a national of a country in Country Groups D:1, +E:1 or E:2 any restricted technology, software, or source code you receive hereunder, or (2) export to Country Groups +D:1, E:1 or E:2 the direct product of such technology or software, if such foreign produced direct product is subject +to national security controls as identified on the Commerce Control List (currently found in Supplement 1 to Part 774 +of EAR). For the most current Country Group listings, or for additional information about the EAR or your obligations +under those regulations, please refer to the U.S. Bureau of Industry and Security's website at http://www.bis.doc.gov/. + +*/ + +package com.amd.aparapi.sample.convolution; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.WindowConstants; + +import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; + +/** + * An example Aparapi application which demonstrates image manipulation via convolution filter + * + * Converted to use int buffer and some performance tweaks by Gary Frost + * http://processing.org/learning/pixels/ + * + * @author Gary Frost + */ +public class Main{ + // http://docs.gimp.org/en/plug-in-convmatrix.html + + final static class ConvolutionFilter{ + private float[] weights; + + private int offset; + + ConvolutionFilter(float _nw, float _n, float ne, float _w, float _o, float _e, float _sw, float _s, float _se, int _offset) { + weights = new float[] { + _nw, + _w, + ne, + _w, + _o, + _e, + _sw, + _s, + _se + }; + offset = _offset; + } + + } + + private static final ConvolutionFilter NONE = new ConvolutionFilter(0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0); + + private static final ConvolutionFilter BLUR = new ConvolutionFilter(1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 0); + + private static final ConvolutionFilter EMBOSS = new ConvolutionFilter(-2f, -1f, 0f, -1f, 1f, 1f, 0f, 1f, 2f, 0); + + public static class ConvolutionKernel extends Kernel{ + + private final float[] filter = new float[9]; + + private final int[] inputData; + + private final int[] outputData; + + private final int width; + + private final int height; + + private int offset; + + public ConvolutionKernel(int _width, int _height, BufferedImage _inputImage, BufferedImage _outputImage) { + inputData = ((DataBufferInt) _inputImage.getRaster().getDataBuffer()).getData(); + outputData = ((DataBufferInt) _outputImage.getRaster().getDataBuffer()).getData(); + width = _width; + height = _height; + + // setExplicit(true); // This gives us a performance boost + // put(inputData); // Because we are using explicit buffer management we must put the imageData array + } + + public void run() { + + int x = getGlobalId(0); + int y = getGlobalId(1); + int lx = getLocalId(0); + int ly = getLocalId(1); + int w = getGlobalSize(0); + int h = getGlobalSize(1); + // System.out.println(x+","+y+" "+lx+","+ly+" "+w+","+h); + if (x > 1 && x < (w - 1) && y > 1 && y < (h - 1)) { + + int result = 0; + // We handle each color separately using rgbshift as an 8 bit mask for red, green, blue + for (int rgbShift = 0; rgbShift < 24; rgbShift += 8) { // 0,8,16 + int channelAccum = 0; + float accum = 0; + + for (int count = 0; count < 9; count++) { + int dx = (count % 3) - 1; // 0,1,2 -> -1,0,1 + int dy = (count / 3) - 1; // 0,1,2 -> -1,0,1 + + int rgb = (inputData[((y + dy) * w) + (x + dx)]); + int channelValue = ((rgb >> rgbShift) & 0xff); + accum += filter[count]; + channelAccum += channelValue * filter[count++]; + + } + channelAccum /= accum; + channelAccum += offset; + channelAccum = max(0, min(channelAccum, 0xff)); + result |= (channelAccum << rgbShift); + } + outputData[y * w + x] = result; + } + } + + public void convolve(ConvolutionFilter _filter) { + System.arraycopy(_filter.weights, 0, filter, 0, _filter.weights.length); + offset = _filter.offset; + put(filter); + execute(Range.create2D(width, height, 8, 8)); + get(outputData); + } + } + + public static final int PAD = 1024; + + public static int padValue(int value) { + return (PAD - (value % PAD)); + } + + public static int padTo(int value) { + return (value + padValue(value)); + } + + public static void main(String[] _args) throws IOException, InterruptedException { + + JFrame frame = new JFrame("Convolution"); + + BufferedImage testCard = ImageIO.read(new File("testcard.jpg")); + + int imageHeight = testCard.getHeight(); + + int imageWidth = testCard.getWidth(); + + final int width = padTo(imageWidth);// now multiple of 64 + + final int height = padTo(imageHeight); // now multiple of 64 + + System.out.println("image width,height=" + width + "," + height); + + final BufferedImage inputImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + inputImage.getGraphics().drawImage(testCard, padValue(imageWidth) / 2, padValue(imageHeight) / 2, null); + final BufferedImage outputImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + outputImage.getGraphics().drawImage(testCard, padValue(imageWidth) / 2, padValue(imageHeight) / 2, null); + final ConvolutionKernel lifeKernel = new ConvolutionKernel(width, height, inputImage, outputImage); + + // Create a component for viewing the offsecreen image + @SuppressWarnings("serial") JComponent viewer = new JComponent(){ + @Override public void paintComponent(Graphics g) { + // if (lifeKernel.isExplicit()) { + // lifeKernel.get(lifeKernel.inputData); // We only pull the imageData when we intend to use it. + // } + g.drawImage(outputImage, 0, 0, width, height, 0, 0, width, height, this); + } + }; + + // Set the default size and add to the frames content pane + viewer.setPreferredSize(new Dimension(width, height)); + frame.getContentPane().add(viewer); + + // Swing housekeeping + frame.pack(); + frame.setVisible(true); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + + ConvolutionFilter filters[] = new ConvolutionFilter[] { + NONE, + BLUR, + EMBOSS, + }; + long start = System.nanoTime(); + for (int i = 0; i < 100; i++) { + for (ConvolutionFilter filter : filters) { + + lifeKernel.convolve(filter); // Work is performed here + + viewer.repaint(); // Request a repaint of the viewer (causes paintComponent(Graphics) to be called later not synchronous + //Thread.sleep(1000); + } + } + System.out.println((System.nanoTime() - start) / 1000000); + + } +} diff --git a/samples/convolution/src/com/amd/aparapi/sample/convolution/Test12x4_4x2.java b/samples/convolution/src/com/amd/aparapi/sample/convolution/Test12x4_4x2.java new file mode 100644 index 00000000..e68ca82c --- /dev/null +++ b/samples/convolution/src/com/amd/aparapi/sample/convolution/Test12x4_4x2.java @@ -0,0 +1,496 @@ +package com.amd.aparapi.sample.convolution; + +import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; + +public class Test12x4_4x2{ + public static void main(String[] _args) { + // globalThreadId, threadId, globalX, globalY, localX, localY + final int[][] test = new int[][] { + { + 0, //globalThreadId + 0,//threadId + 0,//globalX + 0,//globalY + 0,//localX + 0 + //localY + }, + { + 1,//globalThreadId + 1,//threadId + 1,//globalX + 0,//globalY + 1,//localX + 0 + //localY + }, + { + 2,//globalThreadId + 2,//threadId + 2,//globalX + 0,//globalY + 2,//localX + 0 + //localY + }, + { + 3,//globalThreadId + 3,//threadId + 3,//globalX + 0,//globalY + 3,//localX + 0 + //localY + }, + { + 4,//globalThreadId + 4,//threadId + 0,//globalX + 1,//globalY + 0,//localX + 1 + //localY + }, + { + 5,//globalThreadId + 5,//threadId + 1,//globalX + 1,//globalY + 1,//localX + 1 + //localY + }, + { + 6,//globalThreadId + 6,//threadId + 2,//globalX + 1,//globalY + 2,//localX + 1 + //localY + }, + { + 7,//globalThreadId + 7,//threadId + 3,//globalX + 1,//globalY + 3,//localX + 1 + //localY + }, + { + 8,//globalThreadId + 0,//threadId + 4,//globalX + 0,//globalY + 0,//localX + 0 + //localY + }, + { + 9,//globalThreadId + 1,//threadId + 5,//globalX + 0,//globalY + 1,//localX + 0 + //localY + }, + { + 10,//globalThreadId + 2,//threadId + 6,//globalX + 0,//globalY + 2,//localX + 0 + //localY + }, + { + 11,//globalThreadId + 3,//threadId + 7,//globalX + 0,//globalY + 3,//localX + 0 + //localY + }, + { + 12,//globalThreadId + 4,//threadId + 4,//globalX + 1,//globalY + 0,//localX + 1 + //localY + }, + { + 13,//globalThreadId + 5,//threadId + 5,//globalX + 1,//globalY + 1,//localX + 1 + //localY + }, + { + 14,//globalThreadId + 6,//threadId + 6,//globalX + 1,//globalY + 2,//localX + 1 + //localY + }, + { + 15,//globalThreadId + 7,//threadId + 7,//globalX + 1,//globalY + 3,//localX + 1 + //localY + }, + { + 16,//globalThreadId + 0,//threadId + 8,//globalX + 0,//globalY + 0,//localX + 0 + //localY + }, + { + 17,//globalThreadId + 1,//threadId + 9,//globalX + 0,//globalY + 1,//localX + 0 + //localY + }, + { + 18,//globalThreadId + 2,//threadId + 10,//globalX + 0,//globalY + 2,//localX + 0 + //localY + }, + { + 19,//globalThreadId + 3,//threadId + 11,//globalX + 0,//globalY + 3,//localX + 0 + //localY + }, + + { + 20,//globalThreadId + 4,//threadId + 8,//globalX + 1,//globalY + 0,//localX + 1 + //localY + }, + { + 21,//globalThreadId + 5,//threadId + 9,//globalX + 1,//globalY + 1,//localX + 1 + //localY + }, + { + 22,//globalThreadId + 6,//threadId + 10,//globalX + 1, + 2,//localX + 1 + //localY + }, + { + 23,//globalThreadId + 7,//threadId + 11,//globalX + 1,//globalY + 3,//localX + 1 + //localY + }, + { + 24,//globalThreadId + 0,//threadId + 0,//globalX + 2,//globalY + 0,//localX + 0 + //localY + }, + { + 25,//globalThreadId + 1,//threadId + 1,//globalX + 2,//globalY + 1,//localX + 0 + //localY + }, + { + 26,//globalThreadId + 2,//threadId + 2,//globalX + 2,//globalY + 2,//localX + 0 + //localY + }, + { + 27,//globalThreadId + 3,//threadId + 3,//globalX + 2,//globalY + 3,//localX + 0 + //localY + }, + { + 28,//globalThreadId + 4,//threadId + 0,//globalX + 3,//globalY + 0,//localX + 1 + //localY + }, + { + 29,//globalThreadId + 5,//threadId + 1,//globalX + 3,//globalY + 1,//localX + 1 + //localY + }, + { + 30,//globalThreadId + 6,//threadId + 2,//globalX + 3,//globalY + 2,//localX + 1 + //localY + }, + { + 31,//globalThreadId + 7,//threadId + 3,//globalX + 3,//globalY + 3,//localX + 1 + //localY + }, + { + 32,//globalThreadId + 0,//threadId + 4,//globalX + 2,//globalY + 0,//localX + 0 + //localY + }, + { + 33,//globalThreadId + 1,//threadId + 5,//globalX + 2,//globalY + 1,//localX + 0 + //localY + }, + { + 34,//globalThreadId + 2,//threadId + 6,//globalX + 2,//globalY + 2,//localX + 0 + //localY + }, + { + 35,//globalThreadId + 3,//threadId + 7,//globalX + 2,//globalY + 3,//localX + 0 + //localY + }, + { + 36,//globalThreadId + 4,//threadId + 4,//globalX + 3,//globalY + 0,//localX + 1 + //localY + }, + { + 37,//globalThreadId + 5,//threadId + 5,//globalX + 3,//globalY + 1,//localX + 1 + //localY + }, + { + 38,//globalThreadId + 6,//threadId + 6,//globalX + 3,//globalY + 2,//localX + 1 + //localY + }, + { + 39,//globalThreadId + 7,//threadId + 7,//globalX + 3,//globalY + 3,//localX + 1 + //localY + }, + { + 40,//globalThreadId + 0,//threadId + 8,//globalX + 2,//globalY + 0,//localX + 0 + //localY + }, + { + 41,//globalThreadId + 1,//threadId + 9,//globalX + 2,//globalY + 1,//localX + 0 + //localY + }, + { + 42,//globalThreadId + 2,//threadId + 10,//globalX + 2,//globalY + 2,//localX + 0 + //localY + }, + { + 43,//globalThreadId + 3,//threadId + 11,//globalX + 2,//globalY + 3,//localX + 0 + //localY + }, + + { + 44,//globalThreadId + 4,//threadId + 8,//globalX + 3,//globalY + 0,//localX + 1 + //localY + }, + { + 45,//globalThreadId + 5,//threadId + 9,//globalX + 3,//globalY + 1,//localX + 1 + //localY + }, + { + 46,//globalThreadId + 6,//threadId + 10,//globalX + 3,//globalY + 2,//localX + 1 + //localY + }, + { + 47,//globalThreadId + 7,//threadId + 11,//globalX + 3,//globalY + 3,//localX + 1 + //localY + }, + }; + Kernel kernel = new Kernel(){ + + @Override public void run() { + int x = getGlobalId(0); + int y = getGlobalId(1); + int lx = getLocalId(0); + int ly = getLocalId(1); + int w = getGlobalSize(0); + int h = getGlobalSize(1); + int globalThreadId = getGlobalId(1) * getGlobalSize(0) + getGlobalId(0); + int threadId = getLocalId(1) * getLocalSize(0) + getLocalId(0); + synchronized (test) { + boolean show = false; + if (globalThreadId != test[globalThreadId][0]) { + System.out.println("bad globalThreadId"); + show = true; + } + if (threadId != test[globalThreadId][1]) { + System.out.println("bad threadId"); + show = true; + } + if (x != test[globalThreadId][2]) { + System.out.println("bad globalx"); + show = true; + } + if (y != test[globalThreadId][3]) { + System.out.println("bad globaly"); + show = true; + } + if (lx != test[globalThreadId][4]) { + System.out.println("bad localx"); + show = true; + } + if (ly != test[globalThreadId][5]) { + System.out.println("bad localy"); + show = true; + } + if (show) { + System.out.println("derived =>" + globalThreadId + " " + threadId + " " + x + "," + y + " " + lx + "," + ly + " " + + w + "," + h); + System.out.println("data =>" + test[globalThreadId][0] + " " + test[globalThreadId][1] + " " + + test[globalThreadId][2] + "," + test[globalThreadId][3] + " " + test[globalThreadId][4] + "," + + test[globalThreadId][5] + " " + w + "," + h); + } + } + } + + }; + kernel.execute(Range.create2D(12, 4, 4, 2)); + + } +} diff --git a/samples/convolution/testcard.jpg b/samples/convolution/testcard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..16b1a709365da51b2247255c903c7c6bb173207f GIT binary patch literal 64477 zcmb@t2V4{1wl6-4iX9uGqC^EjDI#5pL_{efMnnijh=PS8A|hZAGAb$pDpEs_2#9o% zrU*nuKnO*;f{Fr3C?cAnO_)sH#&gc^opbKJ|9AiQz2P$<*|TT%+H0@!U2CrqeiVL# zRvk7nHG<~Np9h(MKS($>@5M0}&nuqZE}oaRs_otb?LA;>E;0<w9ao_R;PX#E2ol*a zp91;|!2i>eA`5qlV9-1P1igb;b6po-So9kD2yI>TS`vcH=31b|&^$RYXdd|c|9J#0 z_(Q|kpW9YJ#%Ka~#{7T$|8oubb8YcI9z543x9ZQYrGJe#|4+}!KdmN^^0{;1$_4Pb z=ugYMzqh2uC;Zi-3a-q9?7-(g`e^)VQTpF4f1Wu5enZeDz>WX(5E+`g{GVPD{@+}i z8+*auD4P#068?l%8l3TTIt!hd8*ZMk7rF(_|LYI|-$WLGPmu)+7t9?C7cKmAELtQg zx>!_n(UK)gmo8bd{9liGA|fIS7c5+|Xwi}tOP4HNA+{16Vq$+f{r%tazy12x`~LbR zd=0H$JkNYyoyfe^(EQc&L{`rezMJ<L5GXot?wBKW?pUx;Wd5Rgkm%wiOP7JgoiFmw zS3v`ydeNVYtDyOSsQC-#FA^02AM?HE%@<j{09v?hjf#QAqP3^Dd)-*48j&h0UU)$I z&GFXnYU`bT$y~nqdxxRAW#r@M&fae|4$4KP6=_PGICG`Ct$(p>(2}JaHr|RpWRzZP z-EOsW6K?={*D*M8*5`IiMoCR4d-9y`o!BR(wOyPkEo1U|zhHW1SzR}GdasF%%hkJa zPhY(MI5e|M+tk*T8WNxNvi{RBDz9_c&donGA-lYxXM_(XJ|9rI5HNDlg2f^t9$-GJ zMHb9aywCzryvXZDglcNxuXS%)ze^twKkjt-x0;N);m!4tmOCChKYuIg-KNohFi7G= z6yVN=jk21DHp!j2Vx`4PE2`ePt9<~u6`fxEuOu5;|8JuIOPc>bNEUtps;({UftHK> zA$B!Hgl0Wa10fX2;8hEuc}f!aqti8(7U;FXd3i!8#5e-0(yMFf5a_G3DESEPnd?u> z2t=K7tHtg5a^{hfMxu_C-|oq6lMh(|`9d`9-_vDVGIMHr{o@QZP2L^(u=b(Iw`I)s zxB8oXhpta@g%HdVLW}ykwMpziKP>&M5Q>Qscwno4uKcHqgnzo`{zmHCxFbA`F@Jdo zyl`?i++ajOZHZkK|Fe5m#B-NXa?Sn_A-%i1+d>E}X0Zgv;k@mue-h+wkJ!aN(UAP5 zp<6S*fB(9ZhaqTcUeSd6#~+vKKbD&w-WnCU-sHLO2ubT3!(G?Xc4c>`k@j;)wc&cw zYsu;PK?1pKxU+2<j|4MID9XU9lsU$OQZPz!zzgWOH3{F<mp)12mpclfwj)f$nn<s~ z-eWg~kisallP824>V;5^?nFwK5F(rP@lENtTODtP8Bcgf0Zidn%}sqKlNQxU92veQ zgiKY1P?HT#kwBz*%dmoca2MdOf}9XqH{Rx-DR5ARhh-^f4uRE=pP?DRF_V2GD9k=1 zg!&V7QnCzj_TM{f+o!*K?Qr<+Z*fyq<wA)0kEb9TeVV*GqiKm9E?b`Jza)n9(chvj zqa`9%(aWPm9#Cp_Bb<q6W9H`<3#LdDh`^nA)SWSTt%EtN3Zn%oL?W1o6@%|eE2T6f zv`k)L40HJ%nVmlNac9e3c6#P3agRqCC^a@e$aKoh9b6T#xDMenCT)e#6Z$BHZGN6# zN}AvaE_fekRrgeA@$zQhmu+GBX&s=GH=S7h$#iQTO{;9NJ-qhT)Pr;&OANj<|C3Z# z7lZeS*v^`K9^Rh76*x{R^N%oa_cy43#LpE%?PimO_=zq!XHW={h+X6V+~+?T9uc@n zZ2DQgEg^xEzW9KFL=<2-mHw&x%fS+x8}+Jt2e9M#CB%12RF7Rz-s_bYZe4wdYr}h~ z6>=Df<Nr(utsT`R@LxE<oks-n)v4_pjHizc67TH$%AMqrJ|!52Y6TP8h&T6r<p`m7 z_<13em1rY`Ufk{$oM6G*o(Q4i!F(J1N#9^4f&YLw5g{<^M<f#EVs|QGb$MobKMSkZ zj7S_TDtrX_C)dAkIh1kl$b-EQF^$ksEq8vX@LhwA*Y$;rgRtdTpC-)$4I^REh5Xo; zImD#mbvpTJI+QQ|QHZ1E8;cz#R<qLY&PV&g#X14Wh0=<bew`l{xudR|!OH1(ppRK6 zDtnr=ND*0s69+FD-40Y`cT`D9okz3@yeN0#^eP}X+3};qhwX5dp+FI~{7I<yCr)mL zho$+6fD0%Cf8ybc_e)SFEeY>knH{NkEHMj?oPbC60&Os!-1ZY|<=pVNWSpS!@Sw*5 zjb#O5GxZ6ui@M#_DPQl+mlinFciw3K+(I}zG*kCe1tGj4bReYP!AZoCOWn+$yQo0a zy!%tLSlDm?t5pcexWH_J1eOy#;)$K+it&>)#%Dtv;m(u9I2MKmH2iZ9_kZmHtI$y% z8iSjB04k}kjFQDVY9=M^J)Dn++jV{mx9&PEf4WQg#K|wIl;_s{j>nh!ra7ZY%x0qM zyQ$sR6Mv})q4e;Pvm#BnC$tcum1i%UfB)zYNc8%o7gkH*vN*Qze@PtyP&fRq)E(9s z*GB8r0}n+t>+tv7c=3FMemy*54cAo&iDYfAYIR9fJ-asAMmMOCv$AF|u;NFPtp3~1 zh7De|h8rJmTV?#>`tL&sm$8$`y@n_YYL$i1NEPjt5RxAH2Zv}I*T#4gHTnix^x=+a z4rTUUpAhN{AiU=jP~#dQB>$ZsRkxcr{+r7nj{JFu=iEbx|8$-Hz=8em_rnnrL|%9p zZdE<0y!jQ2S(g$3<a@l4IlYkyq-ExxNpRHmqr(p{R0vu2#lt_<V6dFOabvIoPo02j zUlN!B0uVy8Sfgj7y)sL1nbU%ZHEDLQ_BYb*?<!e%D$w9V2y$Bp-6WoO_3~X$b*_FO zW8J@6G;U<!OFPr|gOc>O!AJoD^mli6k`Vg2@9EqWQH`&<IGMs##vPd2Lg@1~;1RNC za+qu%{IU>Y_aY2IT?v6-5c=wW+-ri42b!W0Xb6XavMMFa5G_VUJx6H|>TDlV{Dovy z@v3b1!_4(6<_f#EzuM#x%6yt1mveB@LYqa_>(1)lTq*zVN(*azn@f3a`Oo`<_eD&* zxQgex6pMRpb|*1=l=QrE5~j0ij)ynR6GA#)<Au--&uHRr>Gndh@WWs3{WaW|@fhw{ zzBi}Ui%b@`A)Q?4=#d@4H$rgL%`?@#Z;5mtO1HA%6a1(CV0l`dw&a$vpHF|+*7<`U zl3PZ;7(dX|?d+7=`*EDSezU7%oXjD@Wy3cb2Nq>MJrO?rm^2ehqhDJ`>@p`{vIKAR z_ZZMeM-BMbC&7@E8CI?1Pi3tfWH#z!*3~bS5izFtv!9@Oyyk*NJsjs$y4fW4^BK7b zhWE}0PlXtIC%vEG!E_AR)<YSvg$tA`D)knBU#2~G<DRjY;L8*afu>oy1>71qllQMb zdp;g}A>A6+5qPihM>Q@HjBg}N8+`HwC}+hRA+$Vr_Sr8fv-+xSFMqeRW6&?-&-&ac zlDVLRn61zCCV}TVm`_lS!p(bdM=^D@1nY#wveHo7p#6)p&HQ_7_sxji6>NMxOYJ4b zw6D-xL6Rfx#Jd}b-{@nagT>Bf_i#j+xYGw^nrsxwkyhT}RnrlAQ_P{LX3^Ja?sb=s zLs%9zi2?DC3FPLy_ZXO%*mt31>R!j=r_Q$9MX@`WTAv_djKNk~rK<nJL-_;cY2fWK zAc+@S!dF+s2~-85{l8P(0q2^Zq;!Q!{q7zqBd)^7Ud^6M>`|U+>VV&}r&VjkHcSW8 zy-8e3#rbN3;is<>U>(7L_~Slx+-J5{;ZVLSHah*?Za0U~%)I)#*rAD-O#2<$YKpxc z=1U%*>7KpwQ|$#1qylU(fiUAT8&(Iu5yAIpFn&G%)p_O7O!xRkzBcKNbmaJP;&i`Y zV3WCSN4Q{y`O9z<**^jR2nl?#^Wi@ih0vSjt_~x{I7X>(D&{ep=_Hh_GsAgx*A<LF zTmFss4T$W9P2s^`H|G(iQ_(5!LD7IN`5#sva|+2$1?}VL1Q9Pi8Zypy@MrLLsE{WE zLtlye$|D6$VyMwX3$`pE6<}4V<WQ!t70t;ILd~Y&q}sUpdpw9h>1eWN`lp`iMf3%W zM%&C>J}4?|k<MIoeco8KnbjTHd$09>js3nx?9q0TTcK}yFl@nrgR9xkweUOFaN0s4 zL^nn(@udi!H3;hlp})~PEfX3&4BmaZK!$1gMF?>>z}#(2u4V<m&)Q<BN-Elm062Dx z7n{TRqS@Q1sdM8cKesD<&zf}X;mU>Xc2m_`95-%S8>0WKJMk0VS}meI%rla{d_)G? zGT1(?iv;7B`Mtba#J*!fS(ef0xdXR%H+_uzv(g<_=Tnaz$&w+DKT$t&QOQnQM8bGo zpXwIF4KIaI()%{WfZ4~Wrz7vleb>RhrfXsV!8a3eU)hXN^n9b%#gg!&%q06e&t9C< zbHSc^8$X>o|9o=e$|L0$E+^fObXua%Gd$I_XXjeeg$#^Ciq}w*Y|+_5AKVazjPo9Q z6B*2H`IG9w7UivU-%j;q1v%@OM+vKrWs}-e857bUg%B?!SRex*4+vm16SY3z#@H`j z8CQ%gLr7po@(&dxi>}cSQy^_EViv!=3#Z#Q-`-rOXM9mne|KE~{2|;Kcv@M)9K0DA z#J$i$AtXzK4LJoe_*rV{!28v%YPjRU0@=vvc?rJqjTUiltKyFr4|xfp1N*{-(9Sp* zAPD8~D722l+Xhx5bDjup;7|!La%O=bY5>1=jeuM1`Qw%;1UV$UiOJuxlgR7r1<=D^ z2z}Bg4jF?n*=xo4CV0kg9=sRp5kjf0*Fl>?8Mtp!2tEHpC;T@uP5<=yI0bIfj(BY2 zTAmM}Wz>3!H8wphzVExAF25t=y3>B7HYW4r$R0P<Q+1oK4mxiLg*HKc5JIh_|Hy6E zR&^~rd|cD?mZ@Atg6V0nhP@x2RAB|rfo<59DTdng#p=KuX<~PjU{#-ksmG*FAbDV* zn!YdRdn}l;8?kc-qDcHe|BH1;((pTd211CXkqM^d1cO;II^b+G4q33omxw(ELzoge z#`%(gqC!ZqivfOy2%$4GIBbDuDaTVLcKo%BmdqiK2?ADR2Mc8700U8hF+aWPEc{QS zkq%>N*fa=QIB@ch0N_z$dT95`V!O2nl(jM#Ek9}I*TnIX;ci09$Q-2#_^nret-+~s zCdPIQdg9iTPY37%CMCI+(bwPf%-fDk$he@EBb=B4JLi8JR2fKz=`O&gY+j)Z5SQ>a z(|x8d!tnYc2II7g?2!kH*O*7Wm4rXEoCQ}k=enE2A39Jnm@Eyrebd$OCrx8iq(}&L zs0g8qWkAQ3af?D2U^PF%3R289+;|uuYzaY>Gj`(>05}@}8v&v)+y{u<`H=}kY@aJp zv*IyW>9sTuxZ!DAP)#l#B3RfUPq?R?HR`V~0P(<oajPC3zmwU(M4cxXd=o%*A-?~I z5Sm;L4<7)MmI6Yq=nE78mXpDNmTU@E$R`P84uSOni?Y=h=HDSsr2fm?2xtSm>scDj z*i}X<ZFFvOhqnYjx^?T$;`v&lw^W0RlH->wF(%zxbwO%gAwiTuoy9MYzLzrL-FeAL z6+(O2k48jILU!=uU)&*DCS`5W4z$Rc8g17p?EUgc6NvW{Ww(79WBYfyUG@-Y(@HIJ z;pQVWiYo<}>v#g)5cirkNDjEpKd^$_dE2~u*j!&>gb!Qk?IT)-TiTH?#=-PiLjgsO z@5Goe2PQL$F3@AzE4LB3Dp$spdj!UYSXxn=A{xIUFJr`zAUpE;_~)fb-6I;dK5GV8 z(rRDL>9#pt=Q~@%)!Ev9?;~j)-FWaF7%LqWLSD-J-_d*P*Ovh8^LtFazttS(U9Pq@ zJEwH9j2^P*z}s5|y55Ip&sozQ7>7-(wE>3{OlFgs2reU6&LnNzaW5r6rWPmZz$lBr zn(P9o6oii-%7@f>#_R`0=v~>#?*tp2Bds-)yNM@e_8s-%RuZE*o<P`L{=|((9D@0> zV<gm+g{M&RR9a!~PNH&2-dfX63hvcsaLcVrbK1jm^uXtRS7VQg^PlEvTRtJvOG`dE zoz`nt*?w15{rJAjF$*EI9e~VC`sg9}!)x3|2>sBedgDv`@Op0pi~*F`GW@-!5PJL* zh(L5j-!PjGJdU9V3gr74o82;_0MG8ILy06j6C?;G9C`^ndg2_Djq%INc<<-KLnkYM zYgh=d`k5C(NNyMi{s3<o9`?!^KaRj)T>ri#jEy?5m03&{Q(t-TJY6}lB}7^?&Z#VX z<ZA*CM?PHiL6^f(6F?ih(1k+i&=+9wc2SuiysiKOEy}ACLZ@3;j9=D5Xv!>{M`H+% z61OskUX>Er%36FXt?b{Y=7p2mK@?CoC?SyWt-~@v+)GmK%2eiZ*%#iongPM=V`oWw z)wJG!xj^b&AX!!YMQR3X^T++S`Y#;TU3KlvicJjHYb47DHqS>cH>_D_5Wh8C<w7uT zx`i1fbrmU~>pg<Gk`<XxR``#O)C!^2lBl+sjXBjLEK1QpvNB(3EF3j!np-z6Q#l+> z8%*Ug8j1A`Ici;F$_WZu+p=q98GCkE@%D6@J0WMdz5n~9^?=<(K5?jQZc@r%QpGbH zfJEsZvB$~8!EU@-G+GFKQ-Xg)5hm>CR$)%m?f%y1C@_pIs0LWEH*Dg}GRmw>onZYG zm^b^+ysH)wIpI-Ewl{7L|JXw%;q$xkdSIOnRsP{K4ES7dV~)x<1psWw!m>Xq%Fud_ z7++j@mkGRU(o=3Y%-!B%a{Tv+rqwjcaY0{dZE-4lEc|ct{3D+M%i$)1tV1ALh;GTU zXPtctH!DzerrM70GowB(zT$RKenrMg=aI_CV@lR)VVY?&->5;`4QAHF_1`kTzF~T% z2=@ZM;#4nh7Jrya{1K+!^gsx$hnv;lACfc*+$J6@h;YgyXn~fcLZ}P00l%9H#QD>D z`Kf|N<_@@7he~MFKg#<GeD&UtL6Z82zE}I{s-m*6!-e<XMQN>GW#KlNX*U*Hg7yNw zpzkQ}mA4I*cPA6M8v}x~aA)fCgM~aY+bAT>=8KzMX~wR3N**npqcx_R2M1~br)nIH zy#mqARp)q~Y)z1D*h@y8b7@!IG{bUfF6g;WiYvng&1J5SX_n;(4nB%G)=`_)?be?) z{*b5-b2nn~1=Lx@?>+Hh)iIvLU;+D<nyVTBPX;}9_QS|1zvzKj270+!;cUVWlY#Kv zrwGffCjADxvB8*Wj&|!6hCZr8Y$(vCPMcSCW5KAOSK#EA3q$KecRd~PQnbj+%yziC zO=i4X9Nr^!4!EyAaF!P#a&Z^NnA~jvqvDnMnSSe5?|%GEUZy!a?c(+2R?>ifL!jGh zA@nf>DHFV7h#vqs!{!U%v>iFN1K2%mP&6t3+k^hEBYV?3PTtT+ulHIIbg_`N>BY2& zI4o=nqFN+kN^HhmAyl`W#l()qb1GAD6(W_^GqoSi<x^%((gtUvV3k>9_0aGzGxA3t zuZ*ZvCvC`CrZbK^o}2O#LQCq#so@^P0SVwkmqzJgV|2?ZK-OcVq>k;ID8X*Q*o2t8 zE^`T%f1U)ZFA+cLvO4{F^giipPlI>f_M0(o9Tuw*OUb)faob~1S=+rwG|ES8j<vE> z#xZvottX(hv$vf�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 G)BgbdQW$yw literal 0 HcmV?d00001 diff --git a/samples/life/src/com/amd/aparapi/sample/life/Main.java b/samples/life/src/com/amd/aparapi/sample/life/Main.java index 4ede78b1..73a0a867 100644 --- a/samples/life/src/com/amd/aparapi/sample/life/Main.java +++ b/samples/life/src/com/amd/aparapi/sample/life/Main.java @@ -55,6 +55,7 @@ import javax.swing.JPanel; import javax.swing.WindowConstants; import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; /** * An example Aparapi application which demonstrates Conways 'Game Of Life'. @@ -101,6 +102,8 @@ public class Main{ private final int height; + private final Range range; + private int fromBase; private int toBase; @@ -109,6 +112,8 @@ public class Main{ imageData = ((DataBufferInt) _image.getRaster().getDataBuffer()).getData(); width = _width; height = _height; + range = Range.create(width * height, 256); + System.out.println("range = " + range); fromBase = height * width; toBase = 0; setExplicit(true); // This gives us a performance boost @@ -160,7 +165,7 @@ public class Main{ fromBase = toBase; toBase = swap; - execute(width * height); + execute(range); } } diff --git a/samples/mandel/mandel2D.bat b/samples/mandel/mandel2D.bat new file mode 100644 index 00000000..93e67e07 --- /dev/null +++ b/samples/mandel/mandel2D.bat @@ -0,0 +1,7 @@ +java ^ + -Djava.library.path=../../com.amd.aparapi.jni ^ + -Dcom.amd.aparapi.executionMode=%1 ^ + -classpath ../../com.amd.aparapi/aparapi.jar;mandel.jar ^ + com.amd.aparapi.sample.mandel.Main2D + + diff --git a/samples/mandel/mandel2D.sh b/samples/mandel/mandel2D.sh new file mode 100644 index 00000000..99a1c45c --- /dev/null +++ b/samples/mandel/mandel2D.sh @@ -0,0 +1,5 @@ +java\ + -Djava.library.path=../../com.amd.aparapi.jni\ + -Dcom.amd.aparapi.executionMode=$1\ + -classpath ../../com.amd.aparapi/aparapi.jar:mandel.jar\ + com.amd.aparapi.sample.mandel.Main2D diff --git a/samples/mandel/src/com/amd/aparapi/sample/mandel/Main.java b/samples/mandel/src/com/amd/aparapi/sample/mandel/Main.java index 5879f785..99d3373c 100644 --- a/samples/mandel/src/com/amd/aparapi/sample/mandel/Main.java +++ b/samples/mandel/src/com/amd/aparapi/sample/mandel/Main.java @@ -53,6 +53,7 @@ import javax.swing.JComponent; import javax.swing.JFrame; import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; /** * An example Aparapi application which displays a view of the Mandelbrot set and lets the user zoom in to a particular point. @@ -164,6 +165,9 @@ public class Main{ /** Height of Mandelbrot view. */ final int height = 768; + /** Mandelbrot image height. */ + final Range range = Range.create(width * height); + /** Maximum iterations for Mandelbrot. */ final int maxIterations = 256; @@ -220,7 +224,7 @@ public class Main{ // Set the default scale and offset, execute the kernel and force a repaint of the viewer. kernel.setScaleAndOffset(defaultScale, -1f, 0f); - kernel.execute(width * height); + kernel.execute(range); System.arraycopy(rgb, 0, imageRgb, 0, rgb.length); viewer.repaint(); @@ -266,7 +270,7 @@ public class Main{ // Set the scale and offset, execute the kernel and force a repaint of the viewer. kernel.setScaleAndOffset(scale, x, y); - kernel.execute(width * height); + kernel.execute(range); System.arraycopy(rgb, 0, imageRgb, 0, rgb.length); viewer.repaint(); } diff --git a/samples/mandel/src/com/amd/aparapi/sample/mandel/Main2D.java b/samples/mandel/src/com/amd/aparapi/sample/mandel/Main2D.java new file mode 100644 index 00000000..5e6b85a3 --- /dev/null +++ b/samples/mandel/src/com/amd/aparapi/sample/mandel/Main2D.java @@ -0,0 +1,277 @@ +/* +Copyright (c) 2010-2011, Advanced Micro Devices, Inc. +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 the copyright holder 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 HOLDER 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. + +If you use the software (in whole or in part), you shall adhere to all applicable U.S., European, and other export +laws, including but not limited to the U.S. Export Administration Regulations ("EAR"), (15 C.F.R. Sections 730 through +774), and E.U. Council Regulation (EC) No 1334/2000 of 22 June 2000. Further, pursuant to Section 740.6 of the EAR, +you hereby certify that, except pursuant to a license granted by the United States Department of Commerce Bureau of +Industry and Security or as otherwise permitted pursuant to a License Exception under the U.S. Export Administration +Regulations ("EAR"), you will not (1) export, re-export or release to a national of a country in Country Groups D:1, +E:1 or E:2 any restricted technology, software, or source code you receive hereunder, or (2) export to Country Groups +D:1, E:1 or E:2 the direct product of such technology or software, if such foreign produced direct product is subject +to national security controls as identified on the Commerce Control List (currently found in Supplement 1 to Part 774 +of EAR). For the most current Country Group listings, or for additional information about the EAR or your obligations +under those regulations, please refer to the U.S. Bureau of Industry and Security's website at http://www.bis.doc.gov/. + +*/ + +package com.amd.aparapi.sample.mandel; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Point; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; + +import javax.swing.JComponent; +import javax.swing.JFrame; + +import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; + +/** + * An example Aparapi application which displays a view of the Mandelbrot set and lets the user zoom in to a particular point. + * + * When the user clicks on the view, this example application will zoom in to the clicked point and zoom out there after. + * On GPU, additional computing units will offer a better viewing experience. On the other hand on CPU, this example + * application might suffer with sub-optimal frame refresh rate as compared to GPU. + * + * @author gfrost + * + */ + +public class Main2D{ + + /** + * An Aparapi Kernel implementation for creating a scaled view of the mandelbrot set. + * + * @author gfrost + * + */ + + public static class MandelKernel extends Kernel{ + + /** RGB buffer used to store the Mandelbrot image. This buffer holds (width * height) RGB values. */ + final private int rgb[]; + + /** Palette used for each iteration value 0..maxIterations. */ + final private int pallette[]; + + /** Maximum iterations we will check for. */ + final private int maxIterations; + + /** Mutable values of scale, offsetx and offsety so that we can modify the zoom level and position of a view. */ + private float scale = .0f; + + private float offsetx = .0f; + + private float offsety = .0f; + + /** + * Initialize the Kernel. + * + * @param _width Mandelbrot image width + * @param _height Mandelbrot image height + * @param _rgb Mandelbrot image RGB buffer + * @param _pallette Mandelbrot image palette + */ + public MandelKernel(int[] _rgb, int[] _pallette) { + + rgb = _rgb; + pallette = _pallette; + maxIterations = pallette.length - 1; + + } + + @Override public void run() { + + /** Determine which RGB value we are going to process (0..RGB.length). */ + int gid = getGlobalId(1) * getGlobalSize(0) + getGlobalId(0); + + /** Translate the gid into an x an y value. */ + float x = (((getGlobalId(0) * scale) - ((scale / 2) * getGlobalSize(0))) / getGlobalSize(0)) + offsetx; + + float y = (((getGlobalId(1) * scale) - ((scale / 2) * getGlobalSize(1))) / getGlobalSize(1)) + offsety; + + int count = 0; + + float zx = x; + float zy = y; + float new_zx = 0f; + + // Iterate until the algorithm converges or until maxIterations are reached. + while (count < maxIterations && zx * zx + zy * zy < 8) { + new_zx = zx * zx - zy * zy + x; + zy = 2 * zx * zy + y; + zx = new_zx; + count++; + } + + // Pull the value out of the palette for this iteration count. + rgb[gid] = pallette[count]; + } + + public void setScaleAndOffset(float _scale, float _offsetx, float _offsety) { + offsetx = _offsetx; + offsety = _offsety; + scale = _scale; + } + + } + + /** User selected zoom-in point on the Mandelbrot view. */ + public static volatile Point to = null; + + @SuppressWarnings("serial") public static void main(String[] _args) { + + JFrame frame = new JFrame("MandelBrot"); + + /** Mandelbrot image height. */ + final Range range = Range.create2D(768, 768); + System.out.println("range= " + range); + + /** Maximum iterations for Mandelbrot. */ + final int maxIterations = 256; + + /** Palette which maps iteration values to RGB values. */ + final int pallette[] = new int[maxIterations + 1]; + + //Initialize palette values + for (int i = 0; i < maxIterations; i++) { + float h = i / (float) maxIterations; + float b = 1.0f - h * h; + pallette[i] = Color.HSBtoRGB(h, 1f, b); + } + + /** Image for Mandelbrot view. */ + final BufferedImage image = new BufferedImage(range.getGlobalSize(0), range.getGlobalSize(1), BufferedImage.TYPE_INT_RGB); + final BufferedImage offscreen = new BufferedImage(range.getGlobalSize(0), range.getGlobalSize(1), BufferedImage.TYPE_INT_RGB); + // Draw Mandelbrot image + JComponent viewer = new JComponent(){ + @Override public void paintComponent(Graphics g) { + + g.drawImage(image, 0, 0, range.getGlobalSize(0), range.getGlobalSize(1), this); + } + }; + + // Set the size of JComponent which displays Mandelbrot image + viewer.setPreferredSize(new Dimension(range.getGlobalSize(0), range.getGlobalSize(1))); + + final Object doorBell = new Object(); + + // Mouse listener which reads the user clicked zoom-in point on the Mandelbrot view + viewer.addMouseListener(new MouseAdapter(){ + @Override public void mouseClicked(MouseEvent e) { + to = e.getPoint(); + synchronized (doorBell) { + doorBell.notify(); + } + } + }); + + // Swing housework to create the frame + frame.getContentPane().add(viewer); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + + // Extract the underlying RGB buffer from the image. + // Pass this to the kernel so it operates directly on the RGB buffer of the image + final int[] rgb = ((DataBufferInt) offscreen.getRaster().getDataBuffer()).getData(); + final int[] imageRgb = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + // Create a Kernel passing the size, RGB buffer and the palette. + final MandelKernel kernel = new MandelKernel(rgb, pallette); + + float defaultScale = 3f; + + // Set the default scale and offset, execute the kernel and force a repaint of the viewer. + kernel.setScaleAndOffset(defaultScale, -1f, 0f); + kernel.execute(range); + System.arraycopy(rgb, 0, imageRgb, 0, rgb.length); + viewer.repaint(); + + // Report target execution mode: GPU or JTP (Java Thread Pool). + System.out.println("Execution mode=" + kernel.getExecutionMode()); + + // Window listener to dispose Kernel resources on user exit. + frame.addWindowListener(new WindowAdapter(){ + public void windowClosing(WindowEvent _windowEvent) { + kernel.dispose(); + System.exit(0); + } + }); + + // Wait until the user selects a zoom-in point on the Mandelbrot view. + while (true) { + + // Wait for the user to click somewhere + while (to == null) { + synchronized (doorBell) { + try { + doorBell.wait(); + } catch (InterruptedException ie) { + ie.getStackTrace(); + } + } + } + + float x = -1f; + float y = 0f; + float scale = defaultScale; + float tox = (float) (to.x - range.getGlobalSize(0) / 2) / range.getGlobalSize(0) * scale; + float toy = (float) (to.y - range.getGlobalSize(1) / 2) / range.getGlobalSize(1) * scale; + + // This is how many frames we will display as we zoom in and out. + int frames = 128; + long startMillis = System.currentTimeMillis(); + for (int sign = -1; sign < 2; sign += 2) { + for (int i = 0; i < frames - 4; i++) { + scale = scale + sign * defaultScale / frames; + x = x - sign * (tox / frames); + y = y - sign * (toy / frames); + + // Set the scale and offset, execute the kernel and force a repaint of the viewer. + kernel.setScaleAndOffset(scale, x, y); + kernel.execute(range); + System.arraycopy(rgb, 0, imageRgb, 0, rgb.length); + viewer.repaint(); + } + } + + long elapsedMillis = System.currentTimeMillis() - startMillis; + System.out.println("FPS = " + frames * 1000 / elapsedMillis); + + // Reset zoom-in point. + to = null; + + } + + } + +} diff --git a/samples/squares/src/com/amd/aparapi/sample/squares/Main.java b/samples/squares/src/com/amd/aparapi/sample/squares/Main.java index 929d02b3..32a1b70b 100644 --- a/samples/squares/src/com/amd/aparapi/sample/squares/Main.java +++ b/samples/squares/src/com/amd/aparapi/sample/squares/Main.java @@ -39,6 +39,7 @@ under those regulations, please refer to the U.S. Bureau of Industry and Securit package com.amd.aparapi.sample.squares; import com.amd.aparapi.Kernel; +import com.amd.aparapi.Range; /** * An example Aparapi application which computes and displays squares of a set of 512 input values. @@ -77,7 +78,8 @@ public class Main{ }; // Execute Kernel. - kernel.execute(512); + + kernel.execute(Range.create(512)); // Report target execution mode: GPU or JTP (Java Thread Pool). System.out.println("Execution mode=" + kernel.getExecutionMode()); diff --git a/test/codegen/build.xml b/test/codegen/build.xml index 1118e3da..5af3d17a 100644 --- a/test/codegen/build.xml +++ b/test/codegen/build.xml @@ -2,8 +2,7 @@ <project name="codegen" default="junit" basedir="."> - <!--<property name="junit.jar" value="/home/gfrost/aparapi/trunk/tools/junit/junit.jar"/>--> - <property name="junit.jar" value="C:\Users\gfrost\javalabs\projects\aparapi\trunk\tools\junit\junit.jar"/> + <property name="junit.jar" value="./junit-4.10.jar"/> <path id="classpath"> <pathelement path="..\..\com.amd.aparapi\aparapi.jar"/> @@ -11,28 +10,22 @@ <pathelement path="classes"/> </path> - <target name="check"> - <fail message="Error:"> - <condition> - <not><isset property="junit.jar"/></not> - </condition> - <![CDATA[ - You will need to edit test/codegen/build.xml - - At present junit.dir is not set. It needs to point to the junit jar file in your junit installation. + <target name="check-junit"> + <condition property="need.to.upload.junit"> + <not><available file="${junit.jar}"/> </not> + </condition> + </target> - You can install/download junit from www.junit.org. - ]]> - </fail> - <available file="${junit.jar}" type="file" property="junit.jar.exists"/> + <target name="check" if="need.to.upload.junit"> + <get dest="."> + <url url="http://repo1.maven.org/maven2/junit/junit/4.10/junit-4.10.jar"/> + </get> <fail message="Error:"> <condition> - <not><isset property="junit.jar.exists"/></not> + <not><available file="${junit.jar}"/> </not> </condition> <![CDATA[ - You will need to edit test/codegen/build.xml - - At present junit.jar is set to ${junit.jar} but that file does not exist + Failed to upload junit from maven repository. ]]> </fail> </target> @@ -43,7 +36,7 @@ <delete dir="src/genjava/com"/> </target> - <target name="junit" depends="clean, check"> + <target name="junit" depends="clean, check-junit, check"> <mkdir dir="classes"/> <javac srcdir="src/java" destdir="classes" debug="on" includeAntRuntime="false" classpathref="classpath" /> -- GitLab