Closed zipCoder933 closed 1 month ago
I did that and it still crashes sometimes: hs_err_pid8720.log
Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
J 2879 org.lwjgl.stb.STBTruetype.nstbtt_GetPackedQuad(JIIIJJJI)V (0 bytes) @ 0x00000210a571341b [0x00000210a57133a0+0x000000000000007b]
J 2955 c2 org.lwjgl.nuklear.NkQueryFontGlyphCallbackI.callback(JJ)V (68 bytes) @ 0x00000210a571aa5c [0x00000210a571a800+0x000000000000025c]
v ~StubRoutines::call_stub
J 3038 org.lwjgl.nuklear.Nuklear.nnk_convert(JJJJJ)I (0 bytes) @ 0x00000210a572707e [0x00000210a5727020+0x000000000000005e]
J 3090 c1 org.lwjgl.nuklear.Nuklear.nk_convert(Lorg/lwjgl/nuklear/NkContext;Lorg/lwjgl/nuklear/NkBuffer;Lorg/lwjgl/nuklear/NkBuffer;Lorg/lwjgl/nuklear/NkBuffer;Lorg/lwjgl/nuklear/NkConvertConfig;)I (39 bytes) @ 0x000002109e03e70c [0x000002109e03e100+0x000000000000060c]
J 4009 c1 com.xbuilders.window.NKWindow.NKrender()V (639 bytes) @ 0x000002109dda3bfc [0x000002109dda15c0+0x000000000000263c]
J 4107 c1 com.xbuilders.engine.ui.gameScene.GameUI.draw()V (125 bytes) @ 0x000002109dd5b084 [0x000002109dd5a500+0x0000000000000b84]
J 4075 c1 com.xbuilders.engine.gameScene.GameScene.render()V (227 bytes) @ 0x000002109dad8bc4 [0x000002109dad7580+0x0000000000001644]
J 3069 c1 com.xbuilders.game.Main.render()V (22 bytes) @ 0x000002109e02f7c4 [0x000002109e02f6e0+0x00000000000000e4]
j com.xbuilders.game.Main.<init>()V+294
j com.xbuilders.game.Main.main([Ljava/lang/String;)V+197
v ~StubRoutines::call_stub
Here is a more complete chunk of code containing the source of the crashes.
public abstract class NKWindow extends BaseWindow {
. . .
//This prevents the byteBuffers from being garbage collected before the render is done with them, thus preventing a JVM crash
private ByteBuffer vertices, elements; //Used to fix the crash bug (https://github.com/LWJGL/lwjgl3/issues/986)
private NkBuffer vbuf, ebuf; //Also stored out of scope to fix the crash bug
private NkConvertConfig config;//Also stored out of scope to fix the crash bug
private static final int AA = NK_ANTI_ALIASING_ON;
private static final int max_vertex_buffer = MAX_VERTEX_BUFFER;
private static final int max_element_buffer = MAX_ELEMENT_BUFFER;
public void NKrender() {
try (MemoryStack stack = stackPush()) {
enableNuklear();
// setup program
glUseProgram(shader.getID());
glUniform1i(uniform_tex, 0);
glUniformMatrix4fv(uniform_proj, false, stack.floats(2.0f / getWidth(), 0.0f, 0.0f, 0.0f,
0.0f, -2.0f / getHeight(), 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f
));
GL11.glViewport(0, 0, getDisplay_width(), getDisplay_height());
}
{
// convert from command queue into draw list and draw to screen
// allocate vertex and element buffer
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ARRAY_BUFFER, max_vertex_buffer, GL_STREAM_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, max_element_buffer, GL_STREAM_DRAW);
// load draw vertices & elements directly into vertex + element buffer
vertices = Objects.requireNonNull(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY, max_vertex_buffer, null));
elements = Objects.requireNonNull(glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY, max_element_buffer, null));
try (MemoryStack stack = stackPush()) {
// fill convert configuration
config = NkConvertConfig.calloc(stack)
.vertex_layout(VERTEX_LAYOUT)
.vertex_size(20)
.vertex_alignment(4)
.tex_null(null_texture)
.circle_segment_count(22)
.curve_segment_count(22)
.arc_segment_count(22)
.global_alpha(1.0f)
.shape_AA(AA)
.line_AA(AA);
// setup buffers to load vertices and elements
vbuf = NkBuffer.malloc(stack);//TODO: If we keep getting crashes, maybe this has to be stored out of scope too?
ebuf = NkBuffer.malloc(stack);
nk_buffer_init_fixed(vbuf, vertices/*, max_vertex_buffer*/);
nk_buffer_init_fixed(ebuf, elements/*, max_element_buffer*/);
nk_convert(ctx, cmds, vbuf, ebuf, config);//<----------------------- This line is causing keeps causing a JVM crash
}
glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
glUnmapBuffer(GL_ARRAY_BUFFER);
// iterate over and execute each draw command
float fb_scale_x = (float) getDisplay_width() / (float) getWidth();
float fb_scale_y = (float) getDisplay_height() / (float) getHeight();
long offset = NULL;
for (NkDrawCommand cmd = nk__draw_begin(ctx, cmds); cmd != null; cmd = nk__draw_next(cmd, cmds, ctx)) {
if (cmd.elem_count() == 0) {
continue;
}
glBindTexture(GL_TEXTURE_2D, cmd.texture().id());
glScissor((int) (cmd.clip_rect().x() * fb_scale_x),
(int) ((getHeight() - (int) (cmd.clip_rect().y() + cmd.clip_rect().h())) * fb_scale_y),
(int) (cmd.clip_rect().w() * fb_scale_x),
(int) (cmd.clip_rect().h() * fb_scale_y)
);
glDrawElements(GL_TRIANGLES, cmd.elem_count(), GL_UNSIGNED_SHORT, offset);
offset += cmd.elem_count() * 2;
}
nk_clear(ctx);
nk_buffer_clear(cmds);
}
disableNuklear();
}
. . .
}
Things allocated using MemoryStack
are freed when the stack is closed, i.e. at the end of the try (...) {}
. Storing them in a variable won't change this. You should allocate them using MemoryUtil
or BufferUtils
(and properly free them later) if the lifecycle won't fit into a try
.
Ok. Where is the safest place to free up the memory?
Do I even need to do it at all? Im assuming that i need to use MemoryUtil.memFree() but i think bytebuffers are garbage collected
When it should be freed depends on what's using it, generally you should try to free it as soon as possible after it is done being used -- read the documentation of each function or the library to determine when that is.
Anything allocated with MemoryUtil
must be freed using it, or it will leak. Buffers allocated with BufferUtils
will be freed when garbage collected, but this may not happen immediately.
See https://github.com/LWJGL/lwjgl3-wiki/wiki/1.3.-Memory-FAQ.
Hey @zipCoder933,
I did that and it still crashes sometimes:
The vertex & element buffers are unrelated to the problem. Those are stored by the OpenGL driver, the Java-side buffers are gone when you call glUnmapBuffer
and you don't need to keep them around.
See the post I linked above please. The crash happens in the NkUserFont::query
callback, you need to look at the code that sets up font rendering. There should be a call to stbtt_InitFont
somewhere, passing the font data as a buffer. There should be a strong reference to that buffer for as long as you're rendering with the font.
So I followed the advice in the other bug report that you posted (https://github.com/LWJGL/lwjgl3/issues/535#issuecomment-590983833) However the same crash still occurs. I put the fontBuffer in a public static context so that it isnt possible for it to become garbage collected.
public class Theme{
public static ByteBuffer fontBuffer;
public static void initialize(NkContext context, boolean largerFonts) throws IOException {
fontBuffer = NKFontUtils.loadFontData("fonts\\Press_Start_2P\\PressStart2P-Regular.ttf", 512 * 1024);
font_24 = NKFontUtils.TTF_assignToNewTexture(fontBuffer, largerFonts ? 26 : 24);
font_22 = NKFontUtils.TTF_assignToNewTexture(fontBuffer, largerFonts ? 24 : 22);
font_18 = NKFontUtils.TTF_assignToNewTexture(fontBuffer, largerFonts ? 20 : 18);
font_12 = NKFontUtils.TTF_assignToNewTexture(fontBuffer, largerFonts ? 14 : 12);
font_10 = NKFontUtils.TTF_assignToNewTexture(fontBuffer, largerFonts ? 12 : 10);
font_9 = NKFontUtils.TTF_assignToNewTexture(fontBuffer, largerFonts ? 10 : 9);
...
}
}
The loadFontData method uses the LWJGL demo repository's "ioResourceToByteBuffer()" method
public class NKFontUtils {
final static int BITMAP_W = 1024;
final static int BITMAP_H = 1024;
public static final ArrayList<ByteBuffer> fontBuffers = new ArrayList<>();
public static ByteBuffer loadFontData(String path) throws IOException {
ByteBuffer buff = ioResourceToByteBuffer(path, 512 * 1024);
fontBuffers.add(buff);
return buff;
}
public static NkUserFont TTF_assignToNewTexture(final ByteBuffer fontBytes, int fontHeight) {
NkUserFont font = NkUserFont.create();
int fontTexID = glGenTextures();
TextureUtils.addTexture(fontTexID);
STBTTFontinfo fontInfo = STBTTFontinfo.create();
STBTTPackedchar.Buffer cdata = STBTTPackedchar.create(95);
float scale;
float descent;
try (MemoryStack stack = stackPush()) {
stbtt_InitFont(fontInfo, fontBytes);
scale = stbtt_ScaleForPixelHeight(fontInfo, fontHeight);
IntBuffer d = stack.mallocInt(1);
stbtt_GetFontVMetrics(fontInfo, null, d, null);
descent = d.get(0) * scale;
ByteBuffer bitmap = memAlloc(BITMAP_W * BITMAP_H);
STBTTPackContext pc = STBTTPackContext.malloc(stack);
stbtt_PackBegin(pc, bitmap, BITMAP_W, BITMAP_H, 0, 1, NULL);
stbtt_PackSetOversampling(pc, 4, 4);
stbtt_PackFontRange(pc, fontBytes, 0, fontHeight, 32, cdata);
stbtt_PackEnd(pc);
// Convert R8 to RGBA8
ByteBuffer texture = memAlloc(BITMAP_W * BITMAP_H * 4);
for (int i = 0; i < bitmap.capacity(); i++) {
texture.putInt((bitmap.get(i) << 24) | 0x00FFFFFF);
}
texture.flip();
glBindTexture(GL_TEXTURE_2D, fontTexID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, BITMAP_W, BITMAP_H, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
memFree(texture);
memFree(bitmap);
}
font
.width((handle, h, text, len) -> {
float text_width = 0;
try (MemoryStack stack = stackPush()) {
IntBuffer unicode = stack.mallocInt(1);
int glyph_len = nnk_utf_decode(text, memAddress(unicode), len);
int text_len = glyph_len;
if (glyph_len == 0) {
return 0;
}
IntBuffer advance = stack.mallocInt(1);
while (text_len <= len && glyph_len != 0) {
if (unicode.get(0) == NK_UTF_INVALID) {
break;
}
/* query currently drawn glyph information */
stbtt_GetCodepointHMetrics(fontInfo, unicode.get(0), advance, null);
text_width += advance.get(0) * scale;
/* offset next glyph */
glyph_len = nnk_utf_decode(text + text_len, memAddress(unicode), len - text_len);
text_len += glyph_len;
}
}
return text_width;
})
.height(fontHeight)
.query((handle, font_height, glyph, codepoint, next_codepoint) -> {
try (MemoryStack stack = stackPush()) {
FloatBuffer x = stack.floats(0.0f);
FloatBuffer y = stack.floats(0.0f);
STBTTAlignedQuad q = STBTTAlignedQuad.malloc(stack);
IntBuffer advance = stack.mallocInt(1);
stbtt_GetPackedQuad(cdata, BITMAP_W, BITMAP_H, codepoint - 32, x, y, q, false);
stbtt_GetCodepointHMetrics(fontInfo, codepoint, advance, null);
NkUserFontGlyph ufg = NkUserFontGlyph.create(glyph);
ufg.width(q.x1() - q.x0());
ufg.height(q.y1() - q.y0());
ufg.offset().set(q.x0(), q.y0() + (fontHeight + descent));
ufg.xadvance(advance.get(0) * scale);
ufg.uv(0).set(q.s0(), q.t0());
ufg.uv(1).set(q.s1(), q.t1());
}
})
.texture(it -> it
.id(fontTexID));
return font;
}
}
Here is a working MRE: NuklearMRE.zip
The challenge is that this crash occurs VERY sparsely in my main application, and so, this makes it very difficult for me to try to reproduce the error in MRE code as well.
The only way I can give you a crash log is if i open this application enough times that Maybe I get lucky and the crash resurfaces?? (As a note, the "hello world" button in the nuklear UI makes the font buffer null, so possibly might cause the crash to occur?)
Anyways, I'm not sure if opening and closing the window many times per second might help?...
It reproduces the crash with two changes:
fontBuffers.add(buff);
at NkFontUtils.java:56
. Setting fontBuffer
to null
won't do anything if there's another strong reference to the instance.Runtime.getRuntime().gc();
to the main loop. It's very hard to reproduce such a crash without actual memory pressure, the GC will never run.With these two changes, font rendering breaks as soon as I press the button and the program usually crashes a few seconds later.
If your application still crashes after making sure there's always a strong reference to the font data, then it must be another unrelated issue.
Interesting. The MRE now indeed crashes when the byteBuffer is garbage collected, but the trace in the crash log isn't coming from nk_convert
like in my application... This one is coming from nk_begin
:
Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
J 1108 org.lwjgl.stb.STBTruetype.nstbtt_GetCodepointHMetrics(JIJJ)V (0 bytes) @ 0x0000022de6c8c3f6 [0x0000022de6c8c3a0+0x0000000000000056]
J 1236 c2 com.xbuilders.window.nuklear.NKFontUtils.lambda$TTF_assignToNewTexture$0(Lorg/lwjgl/stb/STBTTFontinfo;FJFJI)F (190 bytes) @ 0x0000022de6c99278 [0x0000022de6c99080+0x00000000000001f8]
J 1180 c1 com.xbuilders.window.nuklear.NKFontUtils$$Lambda$63+0x0000000800c308d8.invoke(JFJI)F (18 bytes) @ 0x0000022ddf9d97a4 [0x0000022ddf9d9720+0x0000000000000084]
J 1179 c1 org.lwjgl.nuklear.NkTextWidthCallbackI.callback(JJ)V (62 bytes) @ 0x0000022ddf9da9dc [0x0000022ddf9da020+0x00000000000009bc]
v ~StubRoutines::call_stub
j org.lwjgl.nuklear.Nuklear.nnk_begin(JJJI)Z+0
j org.lwjgl.nuklear.Nuklear.nk_begin(Lorg/lwjgl/nuklear/NkContext;Ljava/lang/CharSequence;Lorg/lwjgl/nuklear/NkRect;I)Z+38
j com.xbuilders.app.TopMenu.render()V+60
Here is a trace of the crash log coming from my real application:
Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
J 3489 org.lwjgl.stb.STBTruetype.nstbtt_GetPackedQuad(JIIIJJJI)V (0 bytes) @ 0x000002517616d09b [0x000002517616d020+0x000000000000007b]
J 3575 c2 org.lwjgl.nuklear.NkQueryFontGlyphCallbackI.callback(JJ)V (68 bytes) @ 0x0000025176178594 [0x0000025176178360+0x0000000000000234]
v ~StubRoutines::call_stub
J 3646 org.lwjgl.nuklear.Nuklear.nnk_convert(JJJJJ)I (0 bytes) @ 0x00000251761833fe [0x00000251761833a0+0x000000000000005e]
J 3889 c2 com.xbuilders.window.NKWindow.NKrender()V (623 bytes) @ 0x00000251761ac058 [0x00000251761ab9c0+0x0000000000000698]
J 4787 c1 com.xbuilders.engine.ui.gameScene.GameUI.draw()V (125 bytes) @ 0x000002516e55e804 [0x000002516e55dc80+0x0000000000000b84]
J 4756 c1 com.xbuilders.engine.gameScene.GameScene.render()V (191 bytes) @ 0x000002516e453eac [0x000002516e453200+0x0000000000000cac]
J 3675 c1 com.xbuilders.game.Main.render()V (22 bytes) @ 0x000002516ebb6844 [0x000002516ebb6760+0x00000000000000e4]
j com.xbuilders.game.Main.<init>()V+294
j com.xbuilders.game.Main.main([Ljava/lang/String;)V+222
v ~StubRoutines::call_stub
Either way, there are indeed 2 strong references to the font byteBuffer in my application, so the font bytebuffer must not be what is causing the crash.
Specifically because this is crashing in GetPackedQuad
, it must be from some memory access in that function. The only pointers used by it are chardata
, xpos
, ypos
, and q
. Since you just stack allocated the last three, I think that only chardata
could possibly be in an invalid state. However, it isn't freed, as all callbacks are GC roots, so there is a strong reference to it as long as the owning NkUserFont
is not freed, which it appears to not be in your MRE at least (held by static field). But, there is a memory access violation that can occur in your code even without any freed memory!
You take codepoint - 32
without checking if codepoint
is at least 32. This means you can pass a negative index to GetPackedQuad
, which isn't caught by LWJGL's checks. This results in GetPackedQuad
accessing below the allocated object, which could occasionally result in an access error. This could be caused by something as simple as the tab or newline character appearing in some text somewhere, as those both have a codepoint below 32.
Try adding a check in your query
callback to ensure that no codepoint is below 32 when you call that method. Whether you hard-crash or just return some dummy data in ufg
, just make sure not to call the GetPackedQuad
method.
Version
3.3.2
Platform
Windows x64
JDK
OpenJDK Runtime Environment Corretto-17.0.11.9.1
Module
org.lwjgl.nuklear.Nuklear
Bug description
I experienced mildly frequent crashes on my LWJGL project. I discovered by reading the hid_pid_err.log that the crashes are coming from Nuklear nnk_convert() method.
Attached is the jvm error log: hs_err_pid11372.log
The code (in my application) that keeps causing the crash is right here:
Stacktrace or crash log output