免费游戏建站-【LWJGL3】LWJGL3的内存分配设计,第

2021-04-18 23:10 admin
--------

免费游戏建站

-------

LWJGL官方网站:

 

作甚“关联”

当大家说,LWJGL 是一个 OpenGL的关联库,这如何了解呢?

最先要了解, OpenGL自身早已是一个完善的图型库,你能够挑选立即应用它的原生态(相对LWJGL来讲的)API,来开展你新项目的开发设计。LWJGL并不是要创建一个新的库,而只是完成能够应用Java語言来开展根据OpenGL的开发设计。LWJGL出示的API,最终還是根据JNI启用native API来完成有关的作用。除因为C和Java在語言特点上的不一样,导致的一些差别外,具体上二者的API从涵数名到涵数签字,都是同样的,这是LWJGL的有意为之,也是“关联”一词的内函。

 

以下将例举几个原生态的API,和LWJGL的API,来直观反映这一点


void glBindTexture(@NativeType("GLenum") int target, @NativeType("GLuint") int texture)
LWJGL3 在运行内存分派方面需要处理的难题

在原生态C語言的OpenGL中,以下这类运行内存分派方法是是非非经常见的

GLuint vbo;
glGenBuffers(1, vbo);

上面这段编码,大家分派界定了一个自变量vbo,这实质上是分派了一块运行内存, glGenBuffers涵数实行完毕后, vbo指向的运行内存地区将被填充一个值,这个值将用于新项目中后续的实际操作。

 

而在Java中,要对这个API开展关联,则需要考虑到:

因为Java不存在根据 取详细地址的英语的语法,因而只能传送long种类的值做为详细地址; 该详细地址指向的是一块堆外运行内存,以便统一位词,本文将统一称之为堆外立即缓存区; 根据JNI开展堆外立即缓存区的申请办理,和上面编码中那样简易的实际操作相比,明显高效率是较低的,因而这样的运行内存分派不宜经常开展; 因而必然要设计方案为“一次分派,数次应用”。

 

大家将从堆外立即缓存区的分派刚开始,逐渐详细介绍处理这些难题的思路。

 

堆外立即缓存区别配

这很简易,假定大家需要4个字节的立即堆外缓存区,大家能够根据 ByteBuffer buffer = ByteBuffer.allocateDirect(4) 来进行分派。

 

怎样得到大家申请办理到的,这块立即堆外缓存区的详细地址?

这相对性比较艰难,尽管 Buffer类(ByteBuffer 类承继了 Buffer)內部有"address" 字段纪录了详细地址信息内容,可是因为以下两个缘故,大家不可以立即获得到它

该字段的装饰符是 private,且沒有出示 public 的 API 用以获得该字段的值; 该字段姓名在不一样服务平台的JDK完成里其实不同样。

因而大家需要这样一个方式,它可以获得一个堆外的特定尺寸的缓存区的详细地址,以下这个涵数的原形就是合乎要求的

public static long getVboBufferAddress(ByteBuffer directByteBuffer)

 

public static long getVboBufferAddress1(ByteBuffer directByteBuffer) throws NoSuchFieldException {
 Field address = Buffer.class.getDeclaredField("address");
 long addressOffset = UNSAFE.objectFieldOffset(address);
 long addr = UNSAFE.getLong(directByteBuffer, addressOffset);
 return addr;
}

 

 

假如要考虑到混合开发,则由于立即堆外缓存区详细地址对应的字段名在不一样服务平台的不一样,就没法应用 Unsafe类的 objectFieldOffset 的方式来获得字段偏移量了。

因而需要另辟蹊径,来获得该字段在目标中的偏移量,这能够选用以下流程来做到:

先应用 JNI 出示的 的 NewDirectByteBuffer 涵数,在特定详细地址处获得一块容量为0的立即堆外立即缓存区,这里有两点需要了解: 该详细地址是特定的,即该详细地址值是一个魔法值。 之因此容量为0,是由于这块缓存区其实不是要用来存物品,而只是用来协助大家,来找到那个储存了立即堆外缓存区详细地址的字段 (在 Oracle JDK 上是名为address的字段)在目标运行内存合理布局中的偏移量。 根据上一步实际操作,大家如今有了一个 ByteBuffer 目标; 对该 ByteBuffer 目标从偏移量为0的详细地址刚开始扫描仪,因为该目标內部毫无疑问有一个long型字段的值为之前特定的魔法值,因而应用魔法值开展逐一比较,就可以找到该字段,同时也就找到了该字段在目标运行内存合理布局中的偏移量。

实际完成以下(这里的魔法值,以便便捷我自身,立即选用了上面编码中一次运作結果的 addr 的值)

/**
 * 考虑到混合开发的状况
 * @param directByteBuffer
 * @return
 * @throws NoSuchFieldException
 public static long getVboBufferAddress2(ByteBuffer directByteBuffer) throws NoSuchFieldException {
 long MATIC_ADDRESS = ;
 ByteBuffer helperBuffer = newDirectByteBuffer(MATIC_ADDRESS, 0);
 long offset = 0;
 while (true) {
 long candidate = UNSAFE.getLong(helperBuffer, offset);
 if (candidate == MATIC_ADDRESS) {
 break;
 } else {
 offset += 8;
 long addr = UNSAFE.getLong(directByteBuffer, offset);
 return addr;
 }

 

这里有一些细节在下也并不是很搞清楚,待在下科学研究清晰后,将会开展升级:

1. 依据在下所知的目标运行内存合理布局的专业知识,64位虚似机下,目标前12个字节是由Mark Word和种类指针构成的目标头,因此基础理论上从第13个字节刚开始检索应当更快,可是在下试了一下,这样是不好的;
2. 从实践活动結果上看,不管目标內部各个字段的排序次序,最后都能根据getLong找到包含魔法值在内的全部long种类字段,而不存在错开的状况。在下猜想这是由于字节对齐导致的,但不清晰是不是只可用于堆外目标。

3. 假如魔法值指向的详细地址早已被实际操作系统软件分派过用于其他主要用途,是不是会有无法意料的危害?

 

但不管怎样,LWJGL3 的确是做了这般的完成,并且上面的编码, 在下也在自身的新项目中做了认证,的确可以一切正常工作中。

 

#include "net_scaventz_test_mem_MyMemUtil.h"
JNIEXPORT jobject _scaventz_test_mem_MyMemUtil_newDirectByteBuffer
(JNIEnv* __env, jclass clazz, jlong address, jlong capacity) {
 void* addr = (void*)(intptr_t) address;
 return (*__env)- NewDirectByteBuffer(__env, addr, capacity);
}

 

难题都处理了吗?不,因为 LWJGL 是一个图型库,纯天然对特性有较高要求,而到此为止,仅仅是以便进行分派一个立即堆外缓存区并得到该缓存区详细地址这么一个实际操作,大家就建立了两个缓存区:

第一个是容量为0的helperBuffer,用于輔助测算详细地址字段在目标中的偏移量。明显该实际操作能够开展优化,只需要在LWJGL起动时实行一次便可以了; 第二个是真实的缓存区directByteBuffer的分派

明显针对 directByteBuffer 的分派,不管怎样相比于应用native api时的那种栈上分派,都是低效的。vbo的分派和应用在OpenGL中是非常经常的实际操作,假如每次需要vbo时都开展一次堆外运行内存分派,将会大大减少 LWJGL 的运作速度。

 

LWJGL1 和 LWJGL2,和别的相近的图型关联库,都是根据分派一次缓存区,随后将这个缓存区缓存文件起来,开展复用来处理的,这自然可以处理难题。但lwjgl的作者并沒有考虑于这类做法,他觉得这样做有以下缺陷:

将致使 ugly code(这将会是工程项目实践活动的工作经验,在下由于沒有应用过 lwjgl2,因而体验不深) 缓存文件起来的 buffer 为 static 自变量,而静态数据自变量没法防止会致使高并发难题

 

作者在 lwjgl3中,根据引入 MemoryStack 类来处理了这个难题

 

MemoryStack

大家不立即贴源码,而是从要求考虑,从处理难题的角度,高瞻远瞩地去了解 MemoryStack 的设计方案思路。

 

大家的要求是:

要防止经常的堆外运行内存分派 要防止应用单例,防止处理高并发难题

不开展经常的分派,就意味着要开展缓存文件,而又不可以仅缓存文件一个案例,这看似是分歧的。

可是思索一下,假如是为每一个进程做一个缓存文件,就恰好能处理了这两个难题。

正好ThreadLocal重要字便可以帮大家进行这件事。
为每一个进程只分派一次堆外缓存区,随后将其储放到 ThreadLocal 里。这类方法即可同时考虑大家的上述两点要求。

 

根据本文到此部位的描述,假如让大家去设计方案MemoryStack,它现阶段的模样应当是以下这样,让大家为他取命叫 MyMemoryStack

public class MyMemoryStack {
 private ByteBuffer directByteBuffer;
 private static ThreadLocal MyMemoryStack tls = ThreadLocal.withInitial(MyMemoryStack::new);
 public MyMemoryStack() {
 directByteBuffer = ByteBuffer.allocateDirect(64 * 1024);
 public static ByteBuffer get() {
 return tls.get().getDirectByteBuffer();
 public ByteBuffer getDirectByteBuffer() {
 return directByteBuffer;
}

 

当大家启用LWJGL3 的 glGenBuffers 涵数时,即可以像以下这样应用 MyMemoryStack

<.scaventz.test.mem; .lwjgl.opengl.GL15C; import java.nio.ByteBuffer; * @author scaventz * @date 2020⑽⒂ public class MyOpenGLBinding { public static int glGenBuffers() { try { ByteBuffer directByteBuffer = MyMemoryStack.get(); long address = MyMemUtil.getVboBufferAddress2(directByteBuffer); // 下面是 LWJGL3 出示的 API,其最后应用 JNI 启用了 native API, // 因为本文的关键不在这里,因此无需关注它的细节 GL15C.nglGenBuffers(1, address); return directByteBuffer.get(); } catch (NoSuchFieldException e) { e.printStackTrace(); return -1; }

 

<.scaventz.test.mem; import java.nio.ByteBuffer; * @author scaventz * @date 2020⑽⒂ public class MyMemoryStack { private ByteBuffer directByteBuffer; private static ThreadLocal MyMemoryStack tls = ThreadLocal.withInitial(MyMemoryStack::new); public MyMemoryStack() { directByteBuffer = ByteBuffer.allocateDirect(64 * 1024); public static ByteBuffer get() { ByteBuffer directByteBuffer = tls.get().getDirectByteBuffer(); directByteBuffer.clear(); return directByteBuffer; public ByteBuffer getDirectByteBuffer() { return directByteBuffer; }

 

<.scaventz.test.mem; import sun.misc.Unsafe; import java.lang.reflect.Field; import java.nio.Buffer; import java.nio.ByteBuffer; * @author scaventz * @date 2020⑽⑿ public class MyMemUtil { private static Unsafe UNSAFE = getUnsafe(); static { System.loadLibrary("mydll"); public static native ByteBuffer newDirectByteBuffer(long address, long capacity); * 不考虑到混合开发的状况 * @param directByteBuffer * @return * @throws NoSuchFieldException public static long getVboBufferAddress1(ByteBuffer directByteBuffer) throws NoSuchFieldException { Field address = Buffer.class.getDeclaredField("address"); long addressOffset = UNSAFE.objectFieldOffset(address); long addr = UNSAFE.getLong(directByteBuffer, addressOffset); return addr; * 考虑到混合开发的状况 * @param directByteBuffer * @return * @throws NoSuchFieldException public static long getVboBufferAddress2(ByteBuffer directByteBuffer) throws NoSuchFieldException { long MAGIC_ADDRESS = ; ByteBuffer helperBuffer = newDirectByteBuffer(MATIC_ADDRESS, 0); long offset = 0; while (true) { long candidate = UNSAFE.getLong(helperBuffer, offset); if (candidate == MAGIC_ADDRESS) { break; } else { offset += 8; long addr = UNSAFE.getLong(directByteBuffer, offset); return addr; private static Unsafe getUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); essible(true); Unsafe unsafe = (Unsafe) field.get(null); return unsafe; } catch (Exception e) { return null; }

Main涵数中,vbo1和vbo2都能一切正常輸出,这说明大家给 nglGenBuffers 传送的 address 值是正确的,MyMemoryStack 如预期一切正常工作中。

 

大气向的难题处理了,接下来需要思索更多的细节。

在同一个进程中,大家常常需要分派多种不一样种类,尺寸各不相同,但生命周期都很短的缓存区,考虑到以下场景:

程序起动时,大家早已为当今进程分派了固定不动尺寸(例如64K bytes)的堆外立即缓存区,堆内引入种类为 ByteBuffer; 在运用中,大家需要持续数次应用 glGenBuffers 这样的 API 时,大家能够将全部 ByteBuffer 的详细地址传入,运用能一切正常工作中;可是,ByteBuffer初次传入以后,下一次应用,则务必优秀行clear。假如有不可以马上clear的场景,则这类大家写的 MyMemoryStack 就不可用; 这类状况下,能够采用另外一种方法,将64K的这个大运行内存,视作一个栈,每次需要分派运行内存时,在这64K里开展划拨出一个栈桢,尺寸最大为64K,随后升级栈顶的部位,当 MemoryStack 的案例超过功效域时,让其全自动实行出栈实际操作(这能够根据让 MemoryStack 完成 Autocloable 插口来完成 具体上 LWJGL 的 MemoryStack 正是这样设计方案的,而且这也是 “Stack” 的内函所属。

 

至此,尽管实际上际的完成由于特性的缘故有很多的优化带来的繁杂性,但MemoryStack 的总体设计方案思路就早已清楚了。

 

MyMemoryStack 的设计方案的确非常完善:

特性方面,该设计方案早已做了非常大的勤奋; 对运行内存的分派和管理方法做了统一解决,编码构造变得清楚; 词义方面也非常优雅,非常是假如你观念到,具体上在原生态API中, GLuint vbo 这样一个实际操作自身就是在实行栈上分派,便更能感受到这类设计方案的美感。

自然 MemoryStack 并不是万金油,因为考虑到到通用性性,MemoryStack 栈尺寸不宜过大,因而不合适用来储放大容量数据信息。

这是 lwjgl 有着一整套运行内存分派对策 的缘故,MemoryStack只是在其中之一,但任何能够应用 MemoryStack 的情况下,都应当优先选择应用它,由于它的高效率是最高的。

---------

免费游戏建站

------------