[RFE] Assimp: How to import from a Virtual FS ?

Started by gobrosse, March 23, 2018, 11:13:33

Previous topic - Next topic

gobrosse

Refactoring my engine to use Assimp, I stumbled upon an issue: there is no way to import an obj+mtl scene from a virtual filesystem.

* There is the possibility of importing directly from memory (aiImportFileFromMemory) but this is advertised as not supporting file formats spread into multiple actual files

* There is the possibility of importing using a common java.io.File, but this would require me to basically extract everything from my virtual filesystem in some cache folder... not cool !

* The original C/Cpp library does provide an interface (AIFileIO) to override for custom FS... but it's not really adapted in the bindings to something Java-friendly, just an auto generated class it seems

In the meantime I'll have to live with option 2, but this feature would be a great plus, as currently I figure serious engines can't really make use of lwjgl's assimp bindings :/

I'm sorry if this was already reported, couldn't find anything using the forum search or Google.
Check out Chunk Stories, a LGPL, mod-friendly, comprehensive Minecraft clone and Voxel Framework

https://chunkstories.xyz

spasi

Quote from: gobrosse on March 23, 2018, 11:13:33but it's not really adapted in the bindings to something Java-friendly, just an auto generated class it seems

Making Java-friendly wrappers on top of the native C API is outside the scope of LWJGL. We don't have that kind of manpower and, more often than not, higher-level wrappers are simply not necessary. IMHO, they add overhead, they can be confusing and users familiar with the native API need to be retrained to use the Java API.

Quote from: gobrosse on March 23, 2018, 11:13:33currently I figure serious engines can't really make use of lwjgl's assimp bindings :/

On the other hand, helping "serious engines" and developers doing commercial work get the most out of LWJGL is our top priority. Something like the following code works fine in the Tootle demo:

private static final Map<String, ByteBuffer> VIRTUAL_FS = new HashMap<>();

public static AIScene loadAssimp(String name, int flags, AIPropertyStore propertyStore) {
    try (MemoryStack stack = stackPush()) {
        AIFileWriteProc writeProc = AIFileWriteProc.create((pFile, pBuffer, memB, count) -> { throw new UnsupportedOperationException(); });
        AIFileFlushProc flushProc = AIFileFlushProc.create(pFile -> { throw new UnsupportedOperationException(); });

        List<AIFile> files = new ArrayList<>();

        AIFileIO fileIO = AIFileIO.callocStack(stack)
            .OpenProc((pFileIO, fileName, openMode) -> {
                ByteBuffer data = VIRTUAL_FS.get(memUTF8(fileName));

                AIFile file = AIFile.callocStack(stack)
                    .ReadProc((pFile, pBuffer, size, count) -> {
                        long remaining = data.remaining();
                        long requested = size * count;

                        long elements = Long.compareUnsigned(requested, remaining) <= 0
                            ? count
                            : Long.divideUnsigned(remaining, size);

                        memCopy(memAddress(data), pBuffer, size * elements);
                        data.position(data.position() + (int)((size * elements) & 0xFFFFFFFF));

                        return elements;
                    })
                    .TellProc(pFile -> Integer.toUnsignedLong(data.position()))
                    .FileSizeProc(pFile -> Integer.toUnsignedLong(data.capacity()))
                    .SeekProc((pFile, offset, origin) -> {
                        long position;
                        switch (origin) {
                            case aiOrigin_SET:
                                position = offset;
                                break;
                            case aiOrigin_CUR:
                                position = data.position() + offset;
                                break;
                            case aiOrigin_END:
                                position = data.capacity() - offset;
                                break;
                            default:
                                throw new IllegalArgumentException();
                        }

                        try {
                            data.position((int)(position & 0xFFFFFFFF));
                        } catch (IllegalArgumentException e) {
                            return -1;
                        }
                        return 0;
                    })
                    .WriteProc(writeProc)
                    .FlushProc(flushProc)
                    .UserData(-1L);

                files.add(file);
                return file.address();
            })
            .CloseProc((pFileIO, pFile) -> {})
            .UserData(-1L);

        try {
            return aiImportFileExWithProperties(name, flags, fileIO, propertyStore);
        } finally {
            for (AIFile file : files) {
                file.ReadProc().free();
                file.TellProc().free();
                file.FileSizeProc().free();
                file.SeekProc().free();
            }

            fileIO.OpenProc().free();
            fileIO.CloseProc().free();

            flushProc.free();
            writeProc.free();
        }
    }
}


Obviously overly simplistic, but I guess you get the idea.

Note: ignore the .UserData(-1L) calls, that's a bug in the bindings that will be fixed soon (the corresponding struct members are currently not nullable).

gobrosse

That example code seems very helpful ! This was a fast reply, guess I won't even have to commit my workarround, thank you.
Check out Chunk Stories, a LGPL, mod-friendly, comprehensive Minecraft clone and Voxel Framework

https://chunkstories.xyz

spasi

I have updated the previous reply to add proper cleanup. The old version was leaking callbacks.

Below you can find a more efficient alternative. It's a bit scary looking and less understandable, but does not have any capturing lambdas and you can reuse the same callback instances for everything:

private static final Map<String, ByteBuffer> VIRTUAL_FS = new HashMap<>();

private static PointerBuffer getAIFileMeta(long pFile) { return memPointerBuffer(memGetAddress(pFile + AIFile.USERDATA), 3); }

private static final AIFileReadProc  AI_FILE_READ_PROC  = AIFileReadProc.create((pFile, pBuffer, size, count) -> {
    PointerBuffer meta = getAIFileMeta(pFile);

    long position = meta.get(1);

    long remaining = meta.get(2) - position;
    long requested = size * count;

    long elements = Long.compareUnsigned(requested, remaining) <= 0
        ? count
        : Long.divideUnsigned(remaining, size);

    memCopy(meta.get(0) + position, pBuffer, size * elements);
    meta.put(1, position + size * elements);

    return elements;
});
private static final AIFileWriteProc AI_FILE_WRITE_PROC = AIFileWriteProc.create((pFile, pBuffer, memB, count) -> {
    throw new UnsupportedOperationException();
});
private static final AIFileTellProc  AI_FILE_TELL_PROC  = AIFileTellProc.create(pFile -> getAIFileMeta(pFile).get(1));
private static final AIFileTellProc  AI_FILE_SIZE_PROC  = AIFileTellProc.create(pFile -> getAIFileMeta(pFile).get(2));
private static final AIFileSeek      AI_FILE_SEEK_PROC  = AIFileSeek.create((pFile, offset, origin) -> {
    PointerBuffer meta = getAIFileMeta(pFile);

    long limit = meta.get(2);

    long position;
    switch (origin) {
        case aiOrigin_SET:
            position = offset;
            break;
        case aiOrigin_CUR:
            position = meta.get(1) + offset;
            break;
        case aiOrigin_END:
            position = limit - offset;
            break;
        default:
            throw new IllegalArgumentException();
    }

    if (position < 0 || limit < position) {
        return -1;
    }
    meta.put(1, position);
    return 0;
});
private static final AIFileFlushProc AI_FILE_FLUSH_PROC = AIFileFlushProc.create(pFile -> { throw new UnsupportedOperationException(); });

private static final AIFileOpenProc  AI_FILE_OPEN_PROC  = AIFileOpenProc.create((pFileIO, fileName, openMode) -> {
    ByteBuffer data = VIRTUAL_FS.get(memUTF8(fileName));

    MemoryStack stack = stackGet();
    return AIFile.callocStack(stack)
        .ReadProc(AI_FILE_READ_PROC)
        .WriteProc(AI_FILE_WRITE_PROC)
        .TellProc(AI_FILE_TELL_PROC)
        .FileSizeProc(AI_FILE_SIZE_PROC)
        .SeekProc(AI_FILE_SEEK_PROC)
        .FlushProc(AI_FILE_FLUSH_PROC)
        // metadata about the file buffer
        .UserData(stack.mallocPointer(3)
            .put(0, memAddress(data)) // origin
            .put(1, 0L) // current position
            .put(2, data.remaining()) // capacity
            .address())
        .address();
});
private static final AIFileCloseProc AI_FILE_CLOSE_PROC = AIFileCloseProc.create((pFileIO, pFile) -> {});

public static AIScene loadAssimp(String name, int flags, AIPropertyStore propertyStore) {
    try (MemoryStack stack = stackPush()) {
        return aiImportFileExWithProperties(
            name,
            flags,
            AIFileIO.callocStack(stack)
                .OpenProc(AI_FILE_OPEN_PROC)
                .CloseProc(AI_FILE_CLOSE_PROC)
                .UserData(-1L),
            propertyStore
        );
    }
}


It's a fine example of using a "user data" pointer and stack allocations to eliminate all overhead. With Project Panama, we'll hopefully be able to use a friendly and type-safe struct/value-type instead of a raw PointerBuffer for the metadata.