Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve GLENUM names/macro/printing #71

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "ModernGL"
uuid = "66fc600b-dfda-50eb-8b99-91cfa97b1301"
version = "1.1.4"
version = "1.2.0"

[deps]
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Build Status](https://github.com/JuliaGL/ModernGL.jl/workflows/CI/badge.svg?branch=master)](https://github.com/JuliaGL/ModernGL.jl/actions)

OpenGL bindings for OpenGL 3.0 and upwards. As OpenGL 3.0 has a lot of overlaps with OpenGL 2.1, OpenGL 2.1 is partly supported as well.
OpenGL bindings for OpenGL 3.0 through 4.6. As OpenGL 3.0 has a lot of overlaps with OpenGL 2.1, OpenGL 2.1 is partly supported as well.

The philosophy is to keep this library strictly a low-level wrapper, so you won't find any error handling (besides for the function loading itself) or abstractions in this package.

Expand All @@ -15,6 +15,11 @@ ENV["MODERNGL_DEBUGGING"] = "true"; Pkg.build("ModernGL")
ENV["MODERNGL_DEBUGGING"] = "false"; Pkg.build("ModernGL")
```

OpenGL constants are wrapped as enums, which allows you to print the name of a constant like this:
GLENUM(x::GLenum).name
This works pretty well, but keep in mind some constants actually have the same value, and only one name will be stored for each value. This leads to counterintuitive behavior in some cases, such as `GLENUM(GL_SYNC_FLUSH_COMMANDS_BIT).name == :GL_TRUE`.
The behavior of GLENUM is manually overridden to return specific names for important constants, like `GL_TRUE` for 1 and `GL_FALSE` for 0. You can force other names using the macro `ModernGL.@custom_glenum [value] [name]`.

### Installation notes
There are no dependencies, besides the graphic driver. If you have any problems, you should consider updating the driver first.

Expand All @@ -28,11 +33,6 @@ Other OpenGL abstraction packages, which make it easier to get started with Open

There might be a few problems with older platforms and video cards, since it's not heavily tested on them.

OpenGL constants are wrapped as enums, which allows you to print the name of a constant like this:
GLENUM(x::GLenum).name
This works pretty well, but some constants actually have the same value. As they're stored in one big dictionary, this leads to some enums being overwritten, resulting in a wrong name being printed.
Most annoying example: `GLENUM(1).name` prints out: `GL_SYNC_FLUSH_COMMANDS_BIT`, but should be `GL_TRUE`

### Some more details

getProcAddress can be changed like this:
Expand Down
98 changes: 73 additions & 25 deletions src/ModernGL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,35 +59,83 @@ isavailable(ptr::Ptr{Cvoid}) = !(
ptr == convert(Ptr{Cvoid}, 3)
)

abstract type Enum end

macro GenEnums(list)
tmp = list.args
enumName = tmp[2]
splice!(tmp, 1:2)
enumType = typeof(eval(tmp[4].args[1].args[2]))
enumdict1 = Dict{enumType, Symbol}()
for elem in tmp
if Meta.isexpr(elem, :const)
enumdict1[eval(elem.args[1].args[2])] = elem.args[1].args[1]
end
struct GLENUM{Sym, T}
number::T
name::Symbol
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I like this to be so heavyweight, with a symbol field and the Sym type parameter...
We already have massive compilation latency problems and putting the name into the type parameter means, that all code using GLENUM will try to specialize on Sym.
Not sure how big the problem is considering that there isn't much to specialize on.. But it still seems excessive!
Maybe it would be easier to group the enums, so that in one group they have their own type, and therefore don't overlap? I don't really see any end user package using the overload infrastructure - this is rather something that should be set in stone by ModernGL itself, or how do you plan to use this?
But it does sounds confusing to me, that when you load some package your enums suddenly get renamed...
Also, does the symbol field actually work in all cases with the C interop? I'm not sure if there are cases where we need to pass an array of enums, or what other problems could arise.
Maybe we can use the implementation from https://github.com/JuliaInterop/CEnum.jl instead, and then put a bit of work into finding some groups, that we need to separate to not get name clashes?

Copy link
Contributor Author

@heyx3 heyx3 Jan 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was imagining that users might want to overload specific values to return whatever name is most relevant to the debugging work they're doing, but you're probably right that it's not a likely scenario in practice.

I'm not sure how much this increases the compilation times, as doesn't the compilation only happen after you first dispatch to it? I thought that in practice, this function is merely something you call once or twice in the REPL while debugging, or maybe 3-10 times when intercepting OpenGL errors. Did I miss a use-case?

I would guess that the project's huge compilation time comes from pre-emptively filling in the GLENUM lookup dictionary, because the GLENUM struct also has a type parameter for the Symbol, which means it's compiling hundreds of specializations upon loading the package. Speaking of which, is there a reason behind that, or would you be interested in a commit which removes it? It seems redundant anyway because it already keeps the Symbol as a field.

I'd be surprised if there are any cirumstances in which you'd send GLENUM to C. Wouldn't external libraries want plain GLenum values, rather than this package's special wrapper?

Copy link
Contributor Author

@heyx3 heyx3 Jan 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimonDanisch Sorry, I think I misunderstood your original comment. I assumed you were talking about my new implementation of the GLENUM constructor, dispatching on Val(UInt32(i)). But it sounds like you were talking about the first type parameter of GLENUM{Sym, T}? That was already there before I made these changes, and as I said above I agree we could get rid of it.

I'm still a bit confused on the circumstances where you'd want to pass a GLENUM into C, as opposed to a plain GLenum.

Also, I just checked over the code, and I was mistaken about compilation times. The lookup dictionary for enums doesn't contain a bunch of GLENUM instances, merely the symbols, so there's no excessive compilation there. The main cost of compilation is most likely the sheer overhead of setting up so many functions and constants, which is probably hard to avoid.

On the note of my new GLENUM() constructor, I just discovered that Julia has a @nospecialize macro, which should guarantee that the compiler only makes 3 overloads -- one for Val{1}, another for Val{0}, and then the catchall for any other Val.

TL;DR I'll cut out the Sym type parameter from GLENUM, and also throw @nospecialize in there.

Copy link
Contributor Author

@heyx3 heyx3 Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After updating the code, it takes about 5 seconds to precompile ModernGL on my machine, and then starting up a window (which involves a number of GL calls, GLFW calls, and also my own code) altogether takes ~3 seconds.

Copy link
Contributor Author

@heyx3 heyx3 Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and as for grouping the GL enums so you can see all the names for each value, that would be nice, but I think it would need extra engineering work. A naive implementation would lead to creating thousands of tiny dictionaries, just for a little debugging helper! I think you'd want some kind of sparse matrix. Replacing the current dictionary with something like that would probably merit its own branch.

For the record, this branch doesn't change how the dictionary lookup works; I just cleaned it up a bit by pulling its declaration out of the macro, and changed GLENUM() from having one method to having three.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I forgot to answer. I did actually missunderstand your PR. I thought it was replacing the constants with const GL_CONSTANT = GLENUM(...), which would put a lot more pressure on the implementation of GLENUM...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense now :D

Well we've got the important bug-fixes merged in, and there's already an experimental branch using Clang, so it's up to you if you still want the other stuff from this PR. I'll try to fix the merge conflicts tonight (this branch should steamroll over the other one).

Either way, do you want some ARB extensions from another branch of mine? I only added a few that were relevant to me, mainly ARB_bindless_textures.

end
Base.print(io::IO, e::GLENUM) = print(io,
"GLENUM(",
e.name, ", ",
e.number,
")"
)
Base.show(io::IO, ::MIME"text/plain", e::GLENUM) = print(io,
e.name,
"<", e.number, ">"
)

const MGL_LOOKUP = Dict{Integer, Symbol}()

"Finds the GLENUM value matching the given number"
GLENUM(i::I) where {I<:Integer} = GLENUM(Val(UInt32(i)))

"Overload this method (with a Val(::UInt32) parameter) to change the name of specific GL constants"
function GLENUM(i::Val)
i_val::Integer = typeof(i).parameters[1]
if haskey(MGL_LOOKUP, i_val)
name::Symbol = MGL_LOOKUP[i_val]
original_type::Type{<:Integer} = typeof(getkey(MGL_LOOKUP, i_val, name))
return GLENUM{name, original_type}(convert(original_type, i_val), name)
else
error(i_val, " is not a valid GLenum value")
end
dictname = gensym()
enumtype = quote
struct $(enumName){Sym, T} <: Enum
number::T
name::Symbol
end
$(dictname) = $enumdict1
function $(enumName)(number::T) where T
if !haskey($(dictname), number)
error("$number is not a GLenum")
end

export GLENUM

macro GenEnums(list::Expr)
if !Meta.isexpr(list, :block)
error("Input to @GenEnums must be a block of expressions of the form 'GL_BLAH = value', optionally with a type if not GLenum.")
end
if length(MGL_LOOKUP) > 0
error("It appears that @GenEnums was invoked more than once. This is not allowed.")
end

entries = list.args
# For each entry, remap it into a more complete expression,
# including exports and a GLENUM() overload.
map!(entries, entries) do entry
if Meta.isexpr(entry, :(=))
value = entry.args[2]
# Find the 'name' and 'type' of the value.
name::Symbol = Symbol()
type::Symbol = Symbol()
if Meta.isexpr(entry.args[1], :(::))
name = entry.args[1].args[1]
type = entry.args[1].args[2]
else
name = entry.args[1]
type = :GLenum
end
$(enumName){$(dictname)[number], T}(number, $(dictname)[number])
# Generate the code.
str_name = string(name)
e_name = esc(name)
e_type = esc(type)
e_value = :(convert($e_type, $(esc(value))))
output = quote
const $e_name = $e_value
ModernGL.MGL_LOOKUP[$e_value] = Symbol($str_name)
export $e_name
end
return output
elseif entry isa LineNumberNode
return entry
else
error("Unexpected statement in block (expected 'GL_BLAH[::GLtype] = value'). \"",
entry, '"')
end

end
esc(Expr(:block, enumtype, tmp..., Expr(:export, :($(enumName)))))
return Expr(:block, entries...)
end

include("glTypes.jl")
Expand Down
Loading