clojurelwjglassimp

Reading a 3D model with lwjgl-assimp failes in Clojure


This is my Clojure code:

(ns learnopengl.mesh-model
  (:import [org.lwjgl.assimp Assimp AINode AIMesh]))

(defn read-model
  "read a 3D model from a file"
  [path]
  (let [scene (Assimp/aiImportFile path (bit-or Assimp/aiProcess_Triangulate
                                                Assimp/aiProcess_FlipUVs))]
    (if (= scene nil)
      (println (Assimp/aiGetErrorString))
      (for [index (range (.mNumMeshes scene))]
        (do
          (println index)
          (AIMesh/create (.get (.mMeshes scene) index)))))))

which fails with:

0
Execution error (IllegalArgumentException) at java.nio.Buffer/createCapacityException (Buffer.java:290).
capacity < 0: (-576307456 < 0)

This happens inside AIMesh/create.

Believing I may have encountered a bug, I rewrote the snippet in Java:

import org.lwjgl.assimp.Assimp;
import org.lwjgl.assimp.AIScene;
import org.lwjgl.assimp.AIMesh;

class Test {
    public static void main(String[] args) {
        AIScene scene = Assimp.aiImportFile("backpack/backpack.obj", Assimp.aiProcess_Triangulate);
        if (scene == null) {
            System.out.println(Assimp.aiGetErrorString());
        } else {
            for (int i = 0; i < scene.mNumMeshes(); i++) {
                System.out.println(i);
                AIMesh mesh = AIMesh.create(scene.mMeshes().get(i));
            }
        }
    }
}

The Java code however runs smoothly. I don't understand why, since I believe the two snippets to be essentially the same. Can someone spot where I may have gone wrong in the Clojure code?

This is the 3D model I've been testing with.

Edit: This is my project.clj:

(defproject learnopengl "0.1.0-SNAPSHOT"
  :description "learnopengl book code"
  :url "learnopengl.com"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.11.1"]
                 [org.lwjgl/lwjgl "3.3.6"]
                 [org.lwjgl/lwjgl "3.3.6" :classifier "natives-linux"]
                 [org.lwjgl/lwjgl-opengl "3.3.6"]
                 [org.lwjgl/lwjgl-opengl "3.3.6" :classifier "natives-linux"]
                 [org.lwjgl/lwjgl-stb "3.3.6"]
                 [org.lwjgl/lwjgl-stb "3.3.6" :classifier "natives-linux"]
                 [org.lwjgl/lwjgl-glfw "3.3.6"]
                 [org.lwjgl/lwjgl-glfw "3.3.6" :classifier "natives-linux"]
                 [org.lwjgl/lwjgl-assimp "3.3.6"]
                 [org.lwjgl/lwjgl-assimp "3.3.6" :classifier "natives-linux"]
                 [org.joml/joml "1.10.8"]]
  :main ^:skip-aot learnopengl.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})

Edit2:

This is my repository with the full code that breaks for me. read-model is defined in mesh-model.clj, and it is called an line 11 in core.clj.

Edit3:

This is the content from the /tmp/clojure-xxx.edn file produced for the crash, which contains the stack trace:

{:clojure.main/message
 "Execution error (IllegalArgumentException) at java.nio.Buffer/createCapacityException (Buffer.java:290).\ncapacity < 0: (-689151616 < 0)\n",
 :clojure.main/triage
 {:clojure.error/class java.lang.IllegalArgumentException,
  :clojure.error/line 290,
  :clojure.error/cause "capacity < 0: (-689151616 < 0)",
  :clojure.error/symbol java.nio.Buffer/createCapacityException,
  :clojure.error/source "Buffer.java",
  :clojure.error/phase :execution},
 :clojure.main/trace
 {:via
  [{:type clojure.lang.Compiler$CompilerException,
    :message
    "Syntax error macroexpanding at (learnopengl/core.clj:12:1).",
    :data
    {:clojure.error/phase :execution,
     :clojure.error/line 12,
     :clojure.error/column 1,
     :clojure.error/source "learnopengl/core.clj"},
    :at [clojure.lang.Compiler load "Compiler.java" 7665]}
   {:type java.lang.IllegalArgumentException,
    :message "capacity < 0: (-689151616 < 0)",
    :at [java.nio.Buffer createCapacityException "Buffer.java" 290]}],
  :trace
  [[java.nio.Buffer createCapacityException "Buffer.java" 290]
   [java.nio.Buffer <init> "Buffer.java" 253]
   [java.nio.ByteBuffer <init> "ByteBuffer.java" 316]
   [java.nio.ByteBuffer <init> "ByteBuffer.java" 324]
   [java.nio.MappedByteBuffer <init> "MappedByteBuffer.java" 113]
   [java.nio.DirectByteBuffer <init> "DirectByteBuffer.java" 107]
   [java.nio.ByteBuffer allocateDirect "ByteBuffer.java" 360]
   [org.lwjgl.system.Struct __create "Struct.java" 118]
   [org.lwjgl.assimp.AIMesh create "AIMesh.java" 487]
   [jdk.internal.reflect.DirectMethodHandleAccessor
    invoke
    "DirectMethodHandleAccessor.java"
    103]
   [java.lang.reflect.Method invoke "Method.java" 580]
   [clojure.lang.Reflector invokeMatchingMethod "Reflector.java" 167]
   [clojure.lang.Reflector invokeStaticMethod "Reflector.java" 332]
   [learnopengl.mesh_model$read_model$iter__332__336$fn__337$fn__338
    invoke
    "mesh_model.clj"
    66]
   [learnopengl.mesh_model$read_model$iter__332__336$fn__337
    invoke
    "mesh_model.clj"
    63]
   [clojure.lang.LazySeq sval "LazySeq.java" 42]
   [clojure.lang.LazySeq seq "LazySeq.java" 51]
   [clojure.lang.RT seq "RT.java" 535]
   [clojure.lang.RT countFrom "RT.java" 650]
   [clojure.lang.RT count "RT.java" 643]
   [learnopengl.core$eval351 invokeStatic "core.clj" 12]
   [learnopengl.core$eval351 invoke "core.clj" 12]
   [clojure.lang.Compiler eval "Compiler.java" 7194]
   [clojure.lang.Compiler load "Compiler.java" 7653]
   [clojure.lang.RT loadResourceScript "RT.java" 381]
   [clojure.lang.RT loadResourceScript "RT.java" 372]
   [clojure.lang.RT load "RT.java" 459]
   [clojure.lang.RT load "RT.java" 424]
   [clojure.core$load$fn__6908 invoke "core.clj" 6161]
   [clojure.core$load invokeStatic "core.clj" 6160]
   [clojure.core$load doInvoke "core.clj" 6144]
   [clojure.lang.RestFn invoke "RestFn.java" 408]
   [clojure.core$load_one invokeStatic "core.clj" 5933]
   [clojure.core$load_one invoke "core.clj" 5928]
   [clojure.core$load_lib$fn__6850 invoke "core.clj" 5975]
   [clojure.core$load_lib invokeStatic "core.clj" 5974]
   [clojure.core$load_lib doInvoke "core.clj" 5953]
   [clojure.lang.RestFn applyTo "RestFn.java" 142]
   [clojure.core$apply invokeStatic "core.clj" 669]
   [clojure.core$load_libs invokeStatic "core.clj" 6016]
   [clojure.core$load_libs doInvoke "core.clj" 6000]
   [clojure.lang.RestFn applyTo "RestFn.java" 137]
   [clojure.core$apply invokeStatic "core.clj" 669]
   [clojure.core$require invokeStatic "core.clj" 6038]
   [clojure.core$require doInvoke "core.clj" 6038]
   [clojure.lang.RestFn invoke "RestFn.java" 408]
   [user$eval140$fn__144 invoke "form-init15974239925031016013.clj" 1]
   [user$eval140 invokeStatic "form-init15974239925031016013.clj" 1]
   [user$eval140 invoke "form-init15974239925031016013.clj" 1]
   [clojure.lang.Compiler eval "Compiler.java" 7194]
   [clojure.lang.Compiler eval "Compiler.java" 7184]
   [clojure.lang.Compiler load "Compiler.java" 7653]
   [clojure.lang.Compiler loadFile "Compiler.java" 7591]
   [clojure.main$load_script invokeStatic "main.clj" 475]
   [clojure.main$init_opt invokeStatic "main.clj" 477]
   [clojure.main$init_opt invoke "main.clj" 477]
   [clojure.main$initialize invokeStatic "main.clj" 508]
   [clojure.main$null_opt invokeStatic "main.clj" 542]
   [clojure.main$null_opt invoke "main.clj" 539]
   [clojure.main$main invokeStatic "main.clj" 664]
   [clojure.main$main doInvoke "main.clj" 616]
   [clojure.lang.RestFn applyTo "RestFn.java" 137]
   [clojure.lang.Var applyTo "Var.java" 705]
   [clojure.main main "main.java" 40]],
  :cause "capacity < 0: (-689151616 < 0)",
  :phase :execution}}

Solution

  • As cfrick has mentioned in a comment under your question, for is unlikely to be suitable here unless the caller of read-model consumes the whole collection.

    The code below is how I'd write it. Note that I also added ^String and ^long to avoid reflection.

    (defn read-model
      "Reads a 3D model from a file and returns a vector of AIMesh pointers."
      [^String path]
      (if-let [scene (Assimp/aiImportFile path (bit-or Assimp/aiProcess_Triangulate
                                                       Assimp/aiProcess_FlipUVs))]
        (mapv (fn [^long index]
                (AIMesh/create (.get (.mMeshes scene) index)))
              (range (.mNumMeshes scene)))
        (throw (ex-info (Assimp/aiGetErrorString) {:path path}))))