rugl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +293 -0
- data/Rakefile +12 -0
- data/examples/batch.rb +63 -0
- data/examples/example_helper.rb +6 -0
- data/examples/framebuffer.rb +76 -0
- data/examples/scope.rb +65 -0
- data/examples/triangle.rb +36 -0
- data/lib/rugl/backends/opengl_bindings.rb +324 -0
- data/lib/rugl/command.rb +236 -0
- data/lib/rugl/command_compiler.rb +192 -0
- data/lib/rugl/context.rb +500 -0
- data/lib/rugl/dynamic.rb +88 -0
- data/lib/rugl/resources/base.rb +93 -0
- data/lib/rugl/resources/buffer.rb +153 -0
- data/lib/rugl/resources/elements.rb +127 -0
- data/lib/rugl/resources/framebuffer.rb +183 -0
- data/lib/rugl/resources/renderbuffer.rb +63 -0
- data/lib/rugl/resources/texture.rb +179 -0
- data/lib/rugl/resources/vao.rb +63 -0
- data/lib/rugl/shader.rb +146 -0
- data/lib/rugl/state/blend.rb +117 -0
- data/lib/rugl/state/color_mask.rb +27 -0
- data/lib/rugl/state/cull.rb +27 -0
- data/lib/rugl/state/depth.rb +33 -0
- data/lib/rugl/state/polygon_offset.rb +40 -0
- data/lib/rugl/state/scissor.rb +40 -0
- data/lib/rugl/state/stencil.rb +96 -0
- data/lib/rugl/state_manager.rb +137 -0
- data/lib/rugl/util.rb +347 -0
- data/lib/rugl/version.rb +5 -0
- data/lib/rugl.rb +44 -0
- metadata +132 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rugl
|
|
4
|
+
class CommandCompiler
|
|
5
|
+
STATE_KEYS = %i[
|
|
6
|
+
depth blend stencil scissor cull polygon_offset color_mask
|
|
7
|
+
front_face line_width dither sample viewport
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(context)
|
|
11
|
+
@context = context
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compile(opts)
|
|
15
|
+
validate_command_opts!(opts)
|
|
16
|
+
|
|
17
|
+
program = get_or_create_program(opts.fetch(:vert), opts.fetch(:frag))
|
|
18
|
+
attributes = compile_attributes(opts[:attributes] || {}, program)
|
|
19
|
+
uniforms = compile_uniforms(opts[:uniforms] || {}, program)
|
|
20
|
+
elements = opts[:elements]
|
|
21
|
+
state = compile_state(opts)
|
|
22
|
+
|
|
23
|
+
draw_params = {
|
|
24
|
+
count: opts[:count],
|
|
25
|
+
primitive: opts.fetch(:primitive, :triangles),
|
|
26
|
+
offset: opts.fetch(:offset, 0),
|
|
27
|
+
instances: opts[:instances]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
vao = create_vao(attributes, elements)
|
|
31
|
+
|
|
32
|
+
Command.new(
|
|
33
|
+
context: @context,
|
|
34
|
+
program: program,
|
|
35
|
+
vao: vao,
|
|
36
|
+
attributes: attributes,
|
|
37
|
+
uniforms: uniforms,
|
|
38
|
+
elements: elements,
|
|
39
|
+
state: state,
|
|
40
|
+
draw_params: draw_params,
|
|
41
|
+
framebuffer: opts[:framebuffer]
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def validate_command_opts!(opts)
|
|
48
|
+
raise CommandError, "command options must be a Hash" unless opts.is_a?(Hash)
|
|
49
|
+
raise CommandError, "command requires :vert shader source" if opts[:vert].to_s.empty?
|
|
50
|
+
raise CommandError, "command requires :frag shader source" if opts[:frag].to_s.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_or_create_program(vert_source, frag_source)
|
|
54
|
+
key = [vert_source.hash, frag_source.hash]
|
|
55
|
+
@context.shader_cache[key] ||= Program.new(
|
|
56
|
+
gl: @context.gl,
|
|
57
|
+
vert_source: vert_source,
|
|
58
|
+
frag_source: frag_source,
|
|
59
|
+
debug: @context.debug
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def compile_attributes(raw_attributes, program)
|
|
64
|
+
raw_attributes.each_with_object({}) do |(name, entry), compiled|
|
|
65
|
+
compiled[name.to_sym] = compile_single_attribute(name.to_sym, entry, program)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def compile_single_attribute(name, entry, program)
|
|
70
|
+
location = program.attribute_location(name)
|
|
71
|
+
|
|
72
|
+
case entry
|
|
73
|
+
when Resources::Buffer
|
|
74
|
+
{
|
|
75
|
+
location: location,
|
|
76
|
+
buffer: entry,
|
|
77
|
+
source: entry,
|
|
78
|
+
dynamic: false,
|
|
79
|
+
size: infer_attribute_size(entry),
|
|
80
|
+
type: entry.type,
|
|
81
|
+
stride: 0,
|
|
82
|
+
offset: 0,
|
|
83
|
+
divisor: nil,
|
|
84
|
+
normalized: false
|
|
85
|
+
}
|
|
86
|
+
when Hash
|
|
87
|
+
buffer_source = entry.fetch(:buffer, entry[:value])
|
|
88
|
+
raise CommandError, "Attribute #{name} is missing :buffer" if buffer_source.nil?
|
|
89
|
+
|
|
90
|
+
dynamic = Dynamic.dynamic?(buffer_source)
|
|
91
|
+
{
|
|
92
|
+
location: location,
|
|
93
|
+
buffer: dynamic ? nil : buffer_source,
|
|
94
|
+
source: buffer_source,
|
|
95
|
+
dynamic: dynamic,
|
|
96
|
+
size: entry.fetch(:size, infer_attribute_size(buffer_source)),
|
|
97
|
+
type: entry.fetch(:type, infer_attribute_type(buffer_source)),
|
|
98
|
+
stride: entry.fetch(:stride, 0),
|
|
99
|
+
offset: entry.fetch(:offset, 0),
|
|
100
|
+
divisor: entry[:divisor],
|
|
101
|
+
normalized: entry.fetch(:normalized, false)
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
raise CommandError, "Attribute #{name} must be a Buffer or Hash definition"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def compile_uniforms(raw_uniforms, program)
|
|
109
|
+
uniform_types = program.uniform_types
|
|
110
|
+
|
|
111
|
+
raw_uniforms.each_with_object({}) do |(name, value), compiled|
|
|
112
|
+
uniform_name = name.to_sym
|
|
113
|
+
type = uniform_types.fetch(uniform_name, infer_uniform_type(value))
|
|
114
|
+
compiled[uniform_name] = {
|
|
115
|
+
value: value,
|
|
116
|
+
type: type,
|
|
117
|
+
location: program.uniform_location(uniform_name)
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def compile_state(opts)
|
|
123
|
+
STATE_KEYS.each_with_object({}) do |key, state|
|
|
124
|
+
state[key] = opts[key] if opts.key?(key)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def create_vao(attributes, elements)
|
|
129
|
+
vao = Resources::Vao.new(@context)
|
|
130
|
+
vao.bind
|
|
131
|
+
|
|
132
|
+
attributes.each_value do |spec|
|
|
133
|
+
next if spec[:dynamic]
|
|
134
|
+
|
|
135
|
+
vao.setup_attribute(
|
|
136
|
+
location: spec[:location],
|
|
137
|
+
buffer: spec[:buffer],
|
|
138
|
+
size: spec[:size],
|
|
139
|
+
type: spec[:type],
|
|
140
|
+
stride: spec[:stride],
|
|
141
|
+
offset: spec[:offset],
|
|
142
|
+
divisor: spec[:divisor],
|
|
143
|
+
normalized: spec[:normalized]
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
elements&.bind
|
|
148
|
+
vao.unbind
|
|
149
|
+
vao
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def infer_attribute_size(buffer_like)
|
|
153
|
+
return 2 unless buffer_like.respond_to?(:length)
|
|
154
|
+
|
|
155
|
+
length = buffer_like.length.to_i
|
|
156
|
+
return 1 if length <= 1
|
|
157
|
+
return 2 if (length % 2).zero?
|
|
158
|
+
return 3 if (length % 3).zero?
|
|
159
|
+
return 4 if (length % 4).zero?
|
|
160
|
+
|
|
161
|
+
1
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def infer_attribute_type(buffer_like)
|
|
165
|
+
return :float unless buffer_like.respond_to?(:type)
|
|
166
|
+
|
|
167
|
+
buffer_like.type
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def infer_uniform_type(value)
|
|
171
|
+
return :sampler2D if value.is_a?(Resources::Texture)
|
|
172
|
+
|
|
173
|
+
return :int if value.is_a?(Integer)
|
|
174
|
+
return :float if value.is_a?(Numeric)
|
|
175
|
+
return :bool if value == true || value == false
|
|
176
|
+
|
|
177
|
+
if value.is_a?(Array)
|
|
178
|
+
case value.length
|
|
179
|
+
when 2 then :vec2
|
|
180
|
+
when 3 then :vec3
|
|
181
|
+
when 4 then :vec4
|
|
182
|
+
when 9 then :mat3
|
|
183
|
+
when 16 then :mat4
|
|
184
|
+
else
|
|
185
|
+
:float
|
|
186
|
+
end
|
|
187
|
+
else
|
|
188
|
+
:float
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
data/lib/rugl/context.rb
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rugl
|
|
4
|
+
class Context
|
|
5
|
+
class NoopStateManager
|
|
6
|
+
def apply(_command_state, context:, props:)
|
|
7
|
+
{}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def restore(_previous_state)
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :window, :debug, :state_manager, :shader_cache, :tick, :pixel_ratio, :gl, :glfw
|
|
16
|
+
|
|
17
|
+
def initialize(
|
|
18
|
+
width: nil,
|
|
19
|
+
height: nil,
|
|
20
|
+
title: "Rugl",
|
|
21
|
+
pixel_ratio: 1.0,
|
|
22
|
+
debug: true,
|
|
23
|
+
version: [4, 1],
|
|
24
|
+
profile: :core,
|
|
25
|
+
samples: nil,
|
|
26
|
+
window: nil,
|
|
27
|
+
gl: nil,
|
|
28
|
+
glfw: nil,
|
|
29
|
+
state_manager: nil,
|
|
30
|
+
auto_init: true,
|
|
31
|
+
**_opts
|
|
32
|
+
)
|
|
33
|
+
@width = width
|
|
34
|
+
@height = height
|
|
35
|
+
@title = title
|
|
36
|
+
@pixel_ratio = pixel_ratio
|
|
37
|
+
@debug = debug
|
|
38
|
+
@version = version
|
|
39
|
+
@profile = profile
|
|
40
|
+
@samples = samples
|
|
41
|
+
@window = window
|
|
42
|
+
@gl = gl
|
|
43
|
+
@glfw = glfw
|
|
44
|
+
@state_manager = state_manager || default_state_manager
|
|
45
|
+
@resources = []
|
|
46
|
+
@shader_cache = {}
|
|
47
|
+
@scope_stack = []
|
|
48
|
+
@start_time = monotonic_time
|
|
49
|
+
@tick = 0
|
|
50
|
+
@running = false
|
|
51
|
+
@destroyed = false
|
|
52
|
+
@compiler = nil
|
|
53
|
+
|
|
54
|
+
setup_default_window if auto_init
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def command(opts)
|
|
58
|
+
@compiler ||= CommandCompiler.new(self)
|
|
59
|
+
@compiler.compile(opts)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def frame(max_frames: nil, &block)
|
|
63
|
+
raise ArgumentError, "frame requires a block" unless block
|
|
64
|
+
raise ContextError, "Context already destroyed" if destroyed?
|
|
65
|
+
|
|
66
|
+
cancel = -> { @running = false }
|
|
67
|
+
@running = true
|
|
68
|
+
frames = 0
|
|
69
|
+
frame_limit = max_frames || default_frame_limit
|
|
70
|
+
|
|
71
|
+
while @running
|
|
72
|
+
break if frame_limit && frames >= frame_limit
|
|
73
|
+
break if window_should_close?
|
|
74
|
+
|
|
75
|
+
poll_events
|
|
76
|
+
@tick += 1
|
|
77
|
+
block.call(build_context)
|
|
78
|
+
swap_buffers
|
|
79
|
+
frames += 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
cancel
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def clear(color: nil, depth: nil, stencil: nil)
|
|
86
|
+
return unless @gl
|
|
87
|
+
|
|
88
|
+
mask = 0
|
|
89
|
+
|
|
90
|
+
if color
|
|
91
|
+
r, g, b, a = normalize_color(color)
|
|
92
|
+
gl_call(:ClearColor, r, g, b, a)
|
|
93
|
+
mask |= gl_const(:COLOR_BUFFER_BIT)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
unless depth.nil?
|
|
97
|
+
if gl_respond_to?(:ClearDepth)
|
|
98
|
+
gl_call(:ClearDepth, depth.to_f)
|
|
99
|
+
elsif gl_respond_to?(:ClearDepthf)
|
|
100
|
+
gl_call(:ClearDepthf, depth.to_f)
|
|
101
|
+
end
|
|
102
|
+
mask |= gl_const(:DEPTH_BUFFER_BIT)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
unless stencil.nil?
|
|
106
|
+
gl_call(:ClearStencil, stencil.to_i) if gl_respond_to?(:ClearStencil)
|
|
107
|
+
mask |= gl_const(:STENCIL_BUFFER_BIT)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
gl_call(:Clear, mask) if mask.positive? && gl_respond_to?(:Clear)
|
|
111
|
+
Util.check_gl_error!(gl: @gl, debug: @debug, message: "clear")
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def read(x: 0, y: 0, width: nil, height: nil)
|
|
116
|
+
read_width, read_height = width_height_or_framebuffer(width, height)
|
|
117
|
+
byte_length = [read_width * read_height * 4, 0].max
|
|
118
|
+
buffer = "\0" * byte_length
|
|
119
|
+
return buffer if @gl.nil?
|
|
120
|
+
|
|
121
|
+
return buffer unless gl_respond_to?(:ReadPixels)
|
|
122
|
+
|
|
123
|
+
gl_call(
|
|
124
|
+
:ReadPixels,
|
|
125
|
+
x.to_i,
|
|
126
|
+
y.to_i,
|
|
127
|
+
read_width,
|
|
128
|
+
read_height,
|
|
129
|
+
gl_const(:RGBA),
|
|
130
|
+
gl_const(:UNSIGNED_BYTE),
|
|
131
|
+
buffer
|
|
132
|
+
)
|
|
133
|
+
buffer
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def destroy
|
|
137
|
+
return if @destroyed
|
|
138
|
+
|
|
139
|
+
@resources.dup.each do |resource|
|
|
140
|
+
next if !resource.respond_to?(:destroyed?) || resource.destroyed?
|
|
141
|
+
|
|
142
|
+
warn "[Rugl] Resource leak detected: #{resource.class}##{resource.respond_to?(:id) ? resource.id : "unknown"}" # rubocop:disable Style/GlobalStdStream
|
|
143
|
+
resource.destroy if resource.respond_to?(:destroy)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if @window && @glfw
|
|
147
|
+
glfw_call(:DestroyWindow, @window) if glfw_respond_to?(:DestroyWindow)
|
|
148
|
+
end
|
|
149
|
+
glfw_call(:Terminate) if @glfw && glfw_respond_to?(:Terminate)
|
|
150
|
+
|
|
151
|
+
@destroyed = true
|
|
152
|
+
@window = nil
|
|
153
|
+
@running = false
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def destroyed?
|
|
158
|
+
@destroyed
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def limits
|
|
162
|
+
{
|
|
163
|
+
max_texture_size: query_integer(:MAX_TEXTURE_SIZE),
|
|
164
|
+
max_viewport_dims: query_integer_array(:MAX_VIEWPORT_DIMS, size: 2),
|
|
165
|
+
max_vertex_attribs: query_integer(:MAX_VERTEX_ATTRIBS),
|
|
166
|
+
version: query_version
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def has_extension?(name)
|
|
171
|
+
return false if @gl.nil?
|
|
172
|
+
|
|
173
|
+
extension_name = name.to_s
|
|
174
|
+
return false if extension_name.empty?
|
|
175
|
+
|
|
176
|
+
if gl_respond_to?(:GetStringi)
|
|
177
|
+
count = query_integer(:NUM_EXTENSIONS)
|
|
178
|
+
return false if count.nil?
|
|
179
|
+
|
|
180
|
+
count.times do |index|
|
|
181
|
+
ext = gl_call(:GetStringi, gl_const(:EXTENSIONS), index)
|
|
182
|
+
return true if ext.to_s == extension_name
|
|
183
|
+
end
|
|
184
|
+
return false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
return false unless gl_respond_to?(:GetString)
|
|
188
|
+
|
|
189
|
+
all_extensions = gl_call(:GetString, gl_const(:EXTENSIONS)).to_s.split
|
|
190
|
+
all_extensions.include?(extension_name)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def current_context
|
|
194
|
+
build_context
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def push_scope(state:, props:)
|
|
198
|
+
@scope_stack << { state: state || {}, props: props || {} }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def pop_scope
|
|
202
|
+
@scope_stack.pop
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def current_scope_props
|
|
206
|
+
@scope_stack.each_with_object({}) do |entry, merged|
|
|
207
|
+
merged.merge!(entry[:props])
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def register_resource(resource)
|
|
212
|
+
@resources << resource
|
|
213
|
+
resource
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def unregister_resource(resource)
|
|
217
|
+
@resources.delete(resource)
|
|
218
|
+
resource
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def buffer(source)
|
|
222
|
+
Resources::Buffer.new(self, source)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def elements(source)
|
|
226
|
+
Resources::Elements.new(self, source)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def texture(source = {})
|
|
230
|
+
Resources::Texture.new(self, source)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def renderbuffer(opts)
|
|
234
|
+
Resources::Renderbuffer.new(self, opts)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def framebuffer(opts)
|
|
238
|
+
Resources::Framebuffer.new(self, opts)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def call_gl(name, *args)
|
|
242
|
+
return nil unless @gl
|
|
243
|
+
|
|
244
|
+
Util.gl_call(@gl, name, *args)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def gl_supports?(name)
|
|
248
|
+
return false unless @gl
|
|
249
|
+
|
|
250
|
+
snake_name = Util.underscore(name)
|
|
251
|
+
@gl.respond_to?(name) || @gl.respond_to?(snake_name)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def gl_constant(name)
|
|
255
|
+
Util.gl_const(name, gl: @gl)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
def default_state_manager
|
|
261
|
+
return NoopStateManager.new unless defined?(StateManager)
|
|
262
|
+
|
|
263
|
+
StateManager.new(gl: @gl)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def setup_default_window
|
|
267
|
+
@gl ||= load_gl_backend
|
|
268
|
+
@glfw ||= load_glfw_backend
|
|
269
|
+
return if @window
|
|
270
|
+
return unless @glfw
|
|
271
|
+
|
|
272
|
+
init_glfw
|
|
273
|
+
create_window
|
|
274
|
+
make_context_current
|
|
275
|
+
load_gl_functions
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def init_glfw
|
|
279
|
+
return unless glfw_respond_to?(:Init)
|
|
280
|
+
|
|
281
|
+
initialized = glfw_call(:Init)
|
|
282
|
+
raise ContextError, "Failed to initialize GLFW" if initialized == false
|
|
283
|
+
|
|
284
|
+
set_window_hints
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def set_window_hints
|
|
288
|
+
return unless glfw_respond_to?(:WindowHint)
|
|
289
|
+
|
|
290
|
+
major, minor = @version
|
|
291
|
+
hint(:CONTEXT_VERSION_MAJOR, major)
|
|
292
|
+
hint(:CONTEXT_VERSION_MINOR, minor)
|
|
293
|
+
hint(:OPENGL_FORWARD_COMPAT, 1)
|
|
294
|
+
hint(:OPENGL_PROFILE, profile_constant(@profile))
|
|
295
|
+
hint(:SAMPLES, @samples) if @samples
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def hint(name, value)
|
|
299
|
+
return unless glfw_has_constant?(name)
|
|
300
|
+
|
|
301
|
+
glfw_call(:WindowHint, glfw_const(name), value)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def profile_constant(profile)
|
|
305
|
+
case profile&.to_sym
|
|
306
|
+
when :core
|
|
307
|
+
glfw_const(:OPENGL_CORE_PROFILE)
|
|
308
|
+
when :compatibility
|
|
309
|
+
glfw_const(:OPENGL_COMPAT_PROFILE)
|
|
310
|
+
else
|
|
311
|
+
glfw_const(:OPENGL_CORE_PROFILE)
|
|
312
|
+
end
|
|
313
|
+
rescue ArgumentError
|
|
314
|
+
0
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def create_window
|
|
318
|
+
return unless glfw_respond_to?(:CreateWindow)
|
|
319
|
+
|
|
320
|
+
width = @width || 800
|
|
321
|
+
height = @height || 600
|
|
322
|
+
@window = glfw_call(:CreateWindow, width, height, @title, nil, nil)
|
|
323
|
+
raise ContextError, "Failed to create GLFW window" if @window.nil?
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def make_context_current
|
|
327
|
+
return unless @window
|
|
328
|
+
return unless glfw_respond_to?(:MakeContextCurrent)
|
|
329
|
+
|
|
330
|
+
glfw_call(:MakeContextCurrent, @window)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def load_gl_functions
|
|
334
|
+
return unless @gl
|
|
335
|
+
|
|
336
|
+
# Supports both `GL.load_lib` and `GL.LoadFunctions` style backends.
|
|
337
|
+
if @gl.respond_to?(:load_lib)
|
|
338
|
+
@gl.load_lib
|
|
339
|
+
elsif @gl.respond_to?(:LoadFunctions)
|
|
340
|
+
@gl.LoadFunctions
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def default_frame_limit
|
|
345
|
+
return nil if @window
|
|
346
|
+
|
|
347
|
+
1
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def poll_events
|
|
351
|
+
return unless @glfw && glfw_respond_to?(:PollEvents)
|
|
352
|
+
|
|
353
|
+
glfw_call(:PollEvents)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def swap_buffers
|
|
357
|
+
return unless @window
|
|
358
|
+
return unless @glfw && glfw_respond_to?(:SwapBuffers)
|
|
359
|
+
|
|
360
|
+
glfw_call(:SwapBuffers, @window)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def window_should_close?
|
|
364
|
+
return false unless @window
|
|
365
|
+
return false unless @glfw && glfw_respond_to?(:WindowShouldClose)
|
|
366
|
+
|
|
367
|
+
glfw_call(:WindowShouldClose, @window) == true
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def build_context
|
|
371
|
+
framebuffer_width, framebuffer_height = framebuffer_size
|
|
372
|
+
viewport_width, viewport_height = viewport_size
|
|
373
|
+
{
|
|
374
|
+
tick: @tick,
|
|
375
|
+
time: monotonic_time - @start_time,
|
|
376
|
+
viewport_width: viewport_width,
|
|
377
|
+
viewport_height: viewport_height,
|
|
378
|
+
framebuffer_width: framebuffer_width,
|
|
379
|
+
framebuffer_height: framebuffer_height,
|
|
380
|
+
drawing_buffer_width: framebuffer_width,
|
|
381
|
+
drawing_buffer_height: framebuffer_height,
|
|
382
|
+
pixel_ratio: @pixel_ratio
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def viewport_size
|
|
387
|
+
return [@width || 0, @height || 0] unless @window && @glfw
|
|
388
|
+
return [@width || 0, @height || 0] unless glfw_respond_to?(:GetWindowSize)
|
|
389
|
+
|
|
390
|
+
glfw_call(:GetWindowSize, @window)
|
|
391
|
+
rescue StandardError
|
|
392
|
+
[@width || 0, @height || 0]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def framebuffer_size
|
|
396
|
+
return [@width || 0, @height || 0] unless @window && @glfw
|
|
397
|
+
return [@width || 0, @height || 0] unless glfw_respond_to?(:GetFramebufferSize)
|
|
398
|
+
|
|
399
|
+
glfw_call(:GetFramebufferSize, @window)
|
|
400
|
+
rescue StandardError
|
|
401
|
+
[@width || 0, @height || 0]
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def width_height_or_framebuffer(width, height)
|
|
405
|
+
fb_width, fb_height = framebuffer_size
|
|
406
|
+
[
|
|
407
|
+
(width || fb_width).to_i,
|
|
408
|
+
(height || fb_height).to_i
|
|
409
|
+
]
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def normalize_color(color)
|
|
413
|
+
values = Array(color).map(&:to_f)
|
|
414
|
+
raise ArgumentError, "Color must include at least 4 elements" if values.length < 4
|
|
415
|
+
|
|
416
|
+
values[0, 4]
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def query_integer(const_name)
|
|
420
|
+
return nil unless @gl
|
|
421
|
+
return nil unless gl_respond_to?(:GetIntegerv)
|
|
422
|
+
|
|
423
|
+
storage = [0]
|
|
424
|
+
gl_call(:GetIntegerv, gl_const(const_name), storage)
|
|
425
|
+
storage.first
|
|
426
|
+
rescue StandardError
|
|
427
|
+
nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def query_integer_array(const_name, size:)
|
|
431
|
+
return nil unless @gl
|
|
432
|
+
return nil unless gl_respond_to?(:GetIntegerv)
|
|
433
|
+
|
|
434
|
+
storage = Array.new(size, 0)
|
|
435
|
+
gl_call(:GetIntegerv, gl_const(const_name), storage)
|
|
436
|
+
storage
|
|
437
|
+
rescue StandardError
|
|
438
|
+
nil
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def query_version
|
|
442
|
+
return nil unless @gl
|
|
443
|
+
return nil unless gl_respond_to?(:GetString)
|
|
444
|
+
|
|
445
|
+
gl_call(:GetString, gl_const(:VERSION)).to_s
|
|
446
|
+
rescue StandardError
|
|
447
|
+
nil
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def gl_call(name, *args)
|
|
451
|
+
Util.gl_call(@gl, name, *args)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def gl_respond_to?(name)
|
|
455
|
+
snake_name = Util.underscore(name)
|
|
456
|
+
@gl && (@gl.respond_to?(name) || @gl.respond_to?(snake_name))
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def gl_const(name)
|
|
460
|
+
Util.gl_const(name, gl: @gl)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def glfw_call(name, *args)
|
|
464
|
+
Util.gl_call(@glfw, name, *args)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def glfw_respond_to?(name)
|
|
468
|
+
snake_name = Util.underscore(name)
|
|
469
|
+
@glfw && (@glfw.respond_to?(name) || @glfw.respond_to?(snake_name))
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def glfw_const(name)
|
|
473
|
+
raise ArgumentError, "GLFW backend is not available" unless @glfw
|
|
474
|
+
|
|
475
|
+
return @glfw.const_get(name) if @glfw.const_defined?(name)
|
|
476
|
+
|
|
477
|
+
raise ArgumentError, "Unknown GLFW constant #{name}"
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def glfw_has_constant?(name)
|
|
481
|
+
@glfw&.const_defined?(name)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def load_gl_backend
|
|
485
|
+
Backends::OpenGLBindingsGL.load
|
|
486
|
+
rescue LoadError, StandardError
|
|
487
|
+
nil
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def load_glfw_backend
|
|
491
|
+
Backends::OpenGLBindingsGLFW.load
|
|
492
|
+
rescue LoadError, StandardError
|
|
493
|
+
nil
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def monotonic_time
|
|
497
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|