teek 0.1.1 → 0.1.2
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 +4 -4
- data/README.md +46 -0
- data/Rakefile +161 -5
- data/ext/teek/extconf.rb +1 -1
- data/ext/teek/tcltkbridge.c +3 -0
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkeventsource.c +195 -0
- data/ext/teek/tkphoto.c +169 -5
- data/ext/teek/tkwin.c +84 -0
- data/lib/teek/background_ractor4x.rb +32 -4
- data/lib/teek/photo.rb +232 -0
- data/lib/teek/version.rb +1 -1
- data/lib/teek.rb +3 -1
- data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
- data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
- data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
- data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
- data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
- data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
- data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
- data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
- data/sample/optcarrot/vendor/optcarrot.rb +14 -0
- data/sample/optcarrot.rb +354 -0
- data/sample/paint/assets/bucket.png +0 -0
- data/sample/paint/assets/cursor.png +0 -0
- data/sample/paint/assets/eraser.png +0 -0
- data/sample/paint/assets/pencil.png +0 -0
- data/sample/paint/assets/spray.png +0 -0
- data/sample/paint/layer.rb +255 -0
- data/sample/paint/layer_manager.rb +179 -0
- data/sample/paint/paint_demo.rb +837 -0
- data/sample/paint/sparse_pixel_buffer.rb +202 -0
- data/sample/sdl2_demo.rb +318 -0
- metadata +29 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92cd230fed0f898dcf23f80a0a6058adcf308e14b4a8f7feb6985026be4692e0
|
|
4
|
+
data.tar.gz: 698f02315ab27973a05490552fbd6aec21447987a726119d583cbc98f197c486
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b259a6ed4a721cfe5179d53eacffdd67f9e3213af17e81eb9c8e434a8857456bf55e2d2798ccddf0099bfe001294fae2cf187111f9aafa575e84e90a1528314e
|
|
7
|
+
data.tar.gz: 20c96ae1cb5f4340737bcd4db2539c01f47f19c7b837bd85fff18ada2d707c4719cabfe0dc14ab1bf986e9b15aee6d61d64883dba4f33f758c3896b9877014c1
|
data/README.md
CHANGED
|
@@ -175,3 +175,49 @@ The debugger provides three tabs:
|
|
|
175
175
|
- **Watches** — right-click or double-click a variable to watch it; tracks last 50 values with timestamps
|
|
176
176
|
|
|
177
177
|
The debugger runs in the same interpreter as your app (as a [Toplevel](https://www.tcl-lang.org/man/tcl8.6/TkCmd/toplevel.htm) window) and filters its own widgets from `app.widgets`.
|
|
178
|
+
|
|
179
|
+
## Background Work
|
|
180
|
+
|
|
181
|
+
Tk applications need to keep the UI responsive while doing CPU-intensive work. The `Teek.background_work` API runs work in a background Ractor with automatic UI integration.
|
|
182
|
+
|
|
183
|
+
**This API is designed for Ruby 4.x.** Ractors on Ruby 3.x lack shareable procs, making them impractical for our use case. A `:thread` mode exists but is rarely beneficial — Ruby threads share the GVL, so thread-based background work often performs *worse* than running inline unless the work involves non-blocking I/O.
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
app = Teek::App.new
|
|
187
|
+
app.show
|
|
188
|
+
app.set_variable('::progress', 0)
|
|
189
|
+
|
|
190
|
+
log = app.create_widget(:text, width: 60, height: 10)
|
|
191
|
+
log.pack(fill: :both, expand: 1, padx: 10, pady: 5)
|
|
192
|
+
|
|
193
|
+
app.create_widget('ttk::progressbar', variable: '::progress', maximum: 100)
|
|
194
|
+
.pack(fill: :x, padx: 10, pady: 5)
|
|
195
|
+
|
|
196
|
+
files = Dir.glob('**/*').select { |f| File.file?(f) }
|
|
197
|
+
|
|
198
|
+
task = Teek::BackgroundWork.new(app, files) do |t, data|
|
|
199
|
+
# Background work goes here - this block cannot access Tk
|
|
200
|
+
data.each_with_index do |file, i|
|
|
201
|
+
t.check_pause
|
|
202
|
+
hash = Digest::SHA256.file(file).hexdigest
|
|
203
|
+
t.yield({ file: file, hash: hash, pct: (i + 1) * 100 / data.size })
|
|
204
|
+
end
|
|
205
|
+
end.on_progress do |msg|
|
|
206
|
+
# This block can access Tk
|
|
207
|
+
log.command(:insert, :end, "#{msg[:file]}: #{msg[:hash]}\n")
|
|
208
|
+
log.command(:see, :end)
|
|
209
|
+
app.set_variable('::progress', msg[:pct])
|
|
210
|
+
end.on_done do
|
|
211
|
+
# This block can also access Tk
|
|
212
|
+
log.command(:insert, :end, "Done!\n")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Control the task
|
|
216
|
+
task.pause # Pause work (resumes at next t.check_pause)
|
|
217
|
+
task.resume # Resume paused work
|
|
218
|
+
task.stop # Stop completely
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The work block runs in a background Ractor and cannot access Tk directly. Use `t.yield()` to send results to `on_progress`, which runs on the main thread where Tk is available. Callbacks (`on_progress`, `on_done`) can be chained in any order.
|
|
222
|
+
|
|
223
|
+
See [`sample/threading_demo.rb`](sample/threading_demo.rb) for a complete file hasher example.
|
data/Rakefile
CHANGED
|
@@ -107,6 +107,7 @@ task yard: 'docs:yard'
|
|
|
107
107
|
# Clean up extconf cached config files
|
|
108
108
|
CLEAN.include('ext/teek/config_list')
|
|
109
109
|
CLOBBER.include('tmp', 'lib/*.bundle', 'lib/*.so', 'ext/**/*.o', 'ext/**/*.bundle', 'ext/**/*.bundle.dSYM')
|
|
110
|
+
CLOBBER.include('teek-sdl2/lib/*.bundle', 'teek-sdl2/lib/*.so', 'teek-sdl2/ext/**/*.o', 'teek-sdl2/ext/**/*.bundle')
|
|
110
111
|
|
|
111
112
|
# Clean coverage artifacts before test runs to prevent accumulation
|
|
112
113
|
CLEAN.include('coverage/.resultset.json', 'coverage/results')
|
|
@@ -119,6 +120,45 @@ if Gem::Specification.find_all_by_name('rake-compiler').any?
|
|
|
119
120
|
ext.ext_dir = 'ext/teek'
|
|
120
121
|
ext.lib_dir = 'lib'
|
|
121
122
|
end
|
|
123
|
+
|
|
124
|
+
Rake::ExtensionTask.new do |ext|
|
|
125
|
+
ext.name = 'teek_sdl2'
|
|
126
|
+
ext.ext_dir = 'teek-sdl2/ext/teek_sdl2'
|
|
127
|
+
ext.lib_dir = 'teek-sdl2/lib'
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
namespace :screenshots do
|
|
132
|
+
desc "Bless current unverified screenshots as the new baselines"
|
|
133
|
+
task :bless do
|
|
134
|
+
require_relative 'test/screenshot_helper'
|
|
135
|
+
src = ScreenshotHelper.unverified_dir
|
|
136
|
+
dst = ScreenshotHelper.blessed_dir
|
|
137
|
+
|
|
138
|
+
pngs = Dir.glob(File.join(src, '*.png'))
|
|
139
|
+
if pngs.empty?
|
|
140
|
+
puts "No unverified screenshots in #{src}"
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
FileUtils.mkdir_p(dst)
|
|
145
|
+
pngs.each do |f|
|
|
146
|
+
FileUtils.cp(f, dst)
|
|
147
|
+
puts " Blessed: #{File.basename(f)}"
|
|
148
|
+
end
|
|
149
|
+
puts "#{pngs.size} screenshot(s) blessed to #{dst}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
desc "Remove unverified screenshots and diffs"
|
|
153
|
+
task :clean do
|
|
154
|
+
require_relative 'test/screenshot_helper'
|
|
155
|
+
[ScreenshotHelper.unverified_dir, ScreenshotHelper.diffs_dir].each do |dir|
|
|
156
|
+
if Dir.exist?(dir)
|
|
157
|
+
FileUtils.rm_rf(dir)
|
|
158
|
+
puts " Removed: #{dir}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
122
162
|
end
|
|
123
163
|
|
|
124
164
|
desc "Clear stale coverage artifacts"
|
|
@@ -178,8 +218,78 @@ def detect_platform
|
|
|
178
218
|
end
|
|
179
219
|
end
|
|
180
220
|
|
|
221
|
+
namespace :sdl2 do
|
|
222
|
+
desc "Compile teek-sdl2 C extension"
|
|
223
|
+
task compile: 'compile:teek_sdl2'
|
|
224
|
+
|
|
225
|
+
Rake::TestTask.new(:test) do |t|
|
|
226
|
+
t.libs << 'teek-sdl2/test' << 'teek-sdl2/lib'
|
|
227
|
+
t.test_files = FileList['teek-sdl2/test/**/test_*.rb'] - FileList['teek-sdl2/test/test_helper.rb']
|
|
228
|
+
t.ruby_opts << '-r test_helper'
|
|
229
|
+
t.verbose = true
|
|
230
|
+
end
|
|
231
|
+
task test: 'compile:teek_sdl2'
|
|
232
|
+
end
|
|
233
|
+
|
|
181
234
|
task :default => :compile
|
|
182
235
|
|
|
236
|
+
namespace :release do
|
|
237
|
+
desc "Build gems, install to temp dir, run smoke test"
|
|
238
|
+
task :smoke do
|
|
239
|
+
require 'tmpdir'
|
|
240
|
+
require 'fileutils'
|
|
241
|
+
|
|
242
|
+
Dir.mktmpdir('teek-smoke') do |tmpdir|
|
|
243
|
+
gem_home = File.join(tmpdir, 'gems')
|
|
244
|
+
|
|
245
|
+
# Build both gems
|
|
246
|
+
puts "Building gems..."
|
|
247
|
+
sh "gem build teek.gemspec -o #{tmpdir}/teek.gem 2>&1"
|
|
248
|
+
Dir.chdir('teek-sdl2') { sh "gem build teek-sdl2.gemspec -o #{tmpdir}/teek-sdl2.gem 2>&1" }
|
|
249
|
+
|
|
250
|
+
# Install into isolated GEM_HOME
|
|
251
|
+
puts "\nInstalling gems..."
|
|
252
|
+
sh "GEM_HOME=#{gem_home} gem install #{tmpdir}/teek.gem --no-document 2>&1"
|
|
253
|
+
sh "GEM_HOME=#{gem_home} gem install #{tmpdir}/teek-sdl2.gem --no-document 2>&1"
|
|
254
|
+
|
|
255
|
+
# Run smoke test using only the installed gems (no -I, no bundle)
|
|
256
|
+
puts "\nRunning SDL2 smoke test..."
|
|
257
|
+
smoke = <<~'RUBY'
|
|
258
|
+
require "teek"
|
|
259
|
+
require "teek/sdl2"
|
|
260
|
+
|
|
261
|
+
app = Teek::App.new
|
|
262
|
+
app.set_window_title("Release Smoke Test")
|
|
263
|
+
app.set_window_geometry("320x240")
|
|
264
|
+
app.show
|
|
265
|
+
app.update
|
|
266
|
+
|
|
267
|
+
vp = Teek::SDL2::Viewport.new(app, width: 300, height: 200)
|
|
268
|
+
vp.pack
|
|
269
|
+
|
|
270
|
+
vp.render do |r|
|
|
271
|
+
r.clear(30, 30, 30)
|
|
272
|
+
r.fill(20, 20, 120, 80, r: 200, g: 50, b: 50)
|
|
273
|
+
r.outline(160, 20, 120, 80, r: 50, g: 200, b: 50)
|
|
274
|
+
r.line(20, 130, 280, 180, r: 50, g: 50, b: 200)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
w, h = vp.renderer.output_size
|
|
278
|
+
pixels = vp.renderer.read_pixels
|
|
279
|
+
raise "read_pixels size mismatch" unless pixels.bytesize == w * h * 4
|
|
280
|
+
|
|
281
|
+
app.after(500) { vp.destroy; app.destroy }
|
|
282
|
+
app.mainloop
|
|
283
|
+
puts "Release smoke test passed (teek #{Teek::VERSION}, teek-sdl2 #{Teek::SDL2::VERSION})"
|
|
284
|
+
RUBY
|
|
285
|
+
|
|
286
|
+
smoke_file = File.join(tmpdir, 'smoke.rb')
|
|
287
|
+
File.write(smoke_file, smoke)
|
|
288
|
+
sh "GEM_HOME=#{gem_home} GEM_PATH=#{gem_home} ruby #{smoke_file}"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
183
293
|
# Docker tasks for local testing and CI
|
|
184
294
|
namespace :docker do
|
|
185
295
|
DOCKERFILE = 'Dockerfile.ci-test'
|
|
@@ -272,8 +382,32 @@ namespace :docker do
|
|
|
272
382
|
Rake::Task['docker:test'].enhance { Rake::Task['docker:prune'].invoke }
|
|
273
383
|
|
|
274
384
|
namespace :test do
|
|
275
|
-
desc "Run tests
|
|
276
|
-
task
|
|
385
|
+
desc "Run teek-sdl2 tests in Docker"
|
|
386
|
+
task sdl2: :build do
|
|
387
|
+
tcl_version = tcl_version_from_env
|
|
388
|
+
ruby_version = ruby_version_from_env
|
|
389
|
+
image_name = docker_image_name(tcl_version, ruby_version)
|
|
390
|
+
|
|
391
|
+
require 'fileutils'
|
|
392
|
+
FileUtils.mkdir_p('coverage')
|
|
393
|
+
|
|
394
|
+
puts "Running teek-sdl2 tests in Docker (Ruby #{ruby_version}, Tcl #{tcl_version})..."
|
|
395
|
+
cmd = "docker run --rm --init"
|
|
396
|
+
cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
|
|
397
|
+
cmd += " -v #{Dir.pwd}/screenshots:/app/screenshots"
|
|
398
|
+
cmd += " -e TCL_VERSION=#{tcl_version}"
|
|
399
|
+
if ENV['COVERAGE'] == '1'
|
|
400
|
+
cmd += " -e COVERAGE=1"
|
|
401
|
+
cmd += " -e COVERAGE_NAME=#{ENV['COVERAGE_NAME'] || 'sdl2'}"
|
|
402
|
+
end
|
|
403
|
+
cmd += " #{image_name}"
|
|
404
|
+
cmd += " xvfb-run -a bundle exec rake sdl2:test"
|
|
405
|
+
|
|
406
|
+
sh cmd
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
desc "Run all tests (teek + teek-sdl2) with coverage and generate report"
|
|
410
|
+
task all: 'docker:build' do
|
|
277
411
|
tcl_version = tcl_version_from_env
|
|
278
412
|
ruby_version = ruby_version_from_env
|
|
279
413
|
image_name = docker_image_name(tcl_version, ruby_version)
|
|
@@ -282,11 +416,17 @@ namespace :docker do
|
|
|
282
416
|
FileUtils.rm_rf('coverage')
|
|
283
417
|
FileUtils.mkdir_p('coverage/results')
|
|
284
418
|
|
|
285
|
-
# Run
|
|
419
|
+
# Run both test suites with coverage enabled and distinct COVERAGE_NAMEs
|
|
286
420
|
ENV['COVERAGE'] = '1'
|
|
287
|
-
|
|
421
|
+
|
|
422
|
+
ENV['COVERAGE_NAME'] = 'main'
|
|
288
423
|
Rake::Task['docker:test'].invoke
|
|
289
424
|
|
|
425
|
+
ENV['COVERAGE_NAME'] = 'sdl2'
|
|
426
|
+
Rake::Task['docker:test:sdl2'].reenable
|
|
427
|
+
Rake::Task['docker:build'].reenable
|
|
428
|
+
Rake::Task['docker:test:sdl2'].invoke
|
|
429
|
+
|
|
290
430
|
# Collate inside Docker (paths match /app/lib/...)
|
|
291
431
|
puts "Collating coverage results..."
|
|
292
432
|
cmd = "docker run --rm --init"
|
|
@@ -304,10 +444,26 @@ namespace :docker do
|
|
|
304
444
|
end
|
|
305
445
|
end
|
|
306
446
|
|
|
447
|
+
namespace :screenshots do
|
|
448
|
+
desc "Bless linux screenshots inside Docker (copies unverified/ to blessed/)"
|
|
449
|
+
task bless: :build do
|
|
450
|
+
ruby_version = ruby_version_from_env
|
|
451
|
+
tcl_version = tcl_version_from_env
|
|
452
|
+
image_name = docker_image_name(tcl_version, ruby_version)
|
|
453
|
+
|
|
454
|
+
cmd = "docker run --rm --init"
|
|
455
|
+
cmd += " -v #{Dir.pwd}/screenshots:/app/screenshots"
|
|
456
|
+
cmd += " #{image_name}"
|
|
457
|
+
cmd += " bundle exec rake screenshots:bless"
|
|
458
|
+
|
|
459
|
+
sh cmd
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
307
463
|
# Scan sample files for # teek-record magic comment
|
|
308
464
|
# Format: # teek-record: title=My Demo, codec=vp9
|
|
309
465
|
def find_recordable_samples
|
|
310
|
-
Dir['sample/**/*.rb'].filter_map do |path|
|
|
466
|
+
Dir['sample/**/*.rb', 'teek-sdl2/sample/**/*.rb'].filter_map do |path|
|
|
311
467
|
first_lines = File.read(path, 500)
|
|
312
468
|
match = first_lines.match(/^#\s*teek-record(?::\s*(.+))?$/)
|
|
313
469
|
next unless match
|
data/ext/teek/extconf.rb
CHANGED
data/ext/teek/tcltkbridge.c
CHANGED
|
@@ -1481,6 +1481,9 @@ Init_tcltklib(void)
|
|
|
1481
1481
|
/* Tk window query functions (tkwin.c) */
|
|
1482
1482
|
Init_tkwin(cInterp);
|
|
1483
1483
|
|
|
1484
|
+
/* External event source integration (tkeventsource.c) */
|
|
1485
|
+
Init_tkeventsource(mTeek);
|
|
1486
|
+
|
|
1484
1487
|
/* Class methods for instance tracking */
|
|
1485
1488
|
rb_define_singleton_method(cInterp, "instance_count", tcltkip_instance_count, 0);
|
|
1486
1489
|
rb_define_singleton_method(cInterp, "instances", tcltkip_instances, 0);
|
data/ext/teek/tcltkbridge.h
CHANGED
|
@@ -39,4 +39,7 @@ void Init_tkfont(VALUE cInterp);
|
|
|
39
39
|
/* Tk window query functions - defined in tkwin.c */
|
|
40
40
|
void Init_tkwin(VALUE cInterp);
|
|
41
41
|
|
|
42
|
+
/* External event source integration - defined in tkeventsource.c */
|
|
43
|
+
void Init_tkeventsource(VALUE mTeek);
|
|
44
|
+
|
|
42
45
|
#endif /* TCLTKBRIDGE_H */
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* tkeventsource.c - External event source integration via Tcl_CreateEventSource
|
|
3
|
+
*
|
|
4
|
+
* Allows other C extensions (e.g. teek-sdl2) to register poll callbacks
|
|
5
|
+
* that run inside Tcl's event loop with zero Ruby overhead in the hot path.
|
|
6
|
+
*
|
|
7
|
+
* The consumer passes a C function pointer via a Ruby method call at
|
|
8
|
+
* registration time. The Tcl event source setup/check procs call that
|
|
9
|
+
* pointer directly — no rb_funcall, no method dispatch.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
#include "tcltkbridge.h"
|
|
13
|
+
#include <stdint.h>
|
|
14
|
+
|
|
15
|
+
static VALUE cEventSource;
|
|
16
|
+
|
|
17
|
+
/* ---------------------------------------------------------
|
|
18
|
+
* Event source struct — wrapped as Ruby TypedData
|
|
19
|
+
* --------------------------------------------------------- */
|
|
20
|
+
|
|
21
|
+
typedef void (*event_source_check_fn)(void *client_data);
|
|
22
|
+
|
|
23
|
+
struct event_source {
|
|
24
|
+
event_source_check_fn check_fn; /* C function pointer from consumer */
|
|
25
|
+
void *client_data; /* Opaque data from consumer */
|
|
26
|
+
Tcl_Time max_block; /* Max block time for setup proc */
|
|
27
|
+
int registered; /* Whether Tcl event source is active */
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/* Forward declarations */
|
|
31
|
+
static void es_setup_proc(ClientData cd, int flags);
|
|
32
|
+
static void es_check_proc(ClientData cd, int flags);
|
|
33
|
+
|
|
34
|
+
/* ---------------------------------------------------------
|
|
35
|
+
* TypedData functions
|
|
36
|
+
* --------------------------------------------------------- */
|
|
37
|
+
|
|
38
|
+
static void
|
|
39
|
+
event_source_free(void *ptr)
|
|
40
|
+
{
|
|
41
|
+
struct event_source *es = ptr;
|
|
42
|
+
if (es->registered) {
|
|
43
|
+
Tcl_DeleteEventSource(es_setup_proc, es_check_proc, (ClientData)es);
|
|
44
|
+
es->registered = 0;
|
|
45
|
+
}
|
|
46
|
+
xfree(es);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static size_t
|
|
50
|
+
event_source_memsize(const void *ptr)
|
|
51
|
+
{
|
|
52
|
+
return sizeof(struct event_source);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static const rb_data_type_t event_source_type = {
|
|
56
|
+
.wrap_struct_name = "Teek::EventSource",
|
|
57
|
+
.function = {
|
|
58
|
+
.dmark = NULL, /* No Ruby VALUEs to mark */
|
|
59
|
+
.dfree = event_source_free,
|
|
60
|
+
.dsize = event_source_memsize,
|
|
61
|
+
},
|
|
62
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/* ---------------------------------------------------------
|
|
66
|
+
* Tcl event source callbacks (hot path — pure C)
|
|
67
|
+
* --------------------------------------------------------- */
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* Setup proc: called before Tcl_WaitForEvent.
|
|
71
|
+
* Caps the block time so our check proc runs frequently.
|
|
72
|
+
*/
|
|
73
|
+
static void
|
|
74
|
+
es_setup_proc(ClientData cd, int flags)
|
|
75
|
+
{
|
|
76
|
+
struct event_source *es = (struct event_source *)cd;
|
|
77
|
+
|
|
78
|
+
if (!(flags & TCL_FILE_EVENTS) && !(flags & TCL_ALL_EVENTS))
|
|
79
|
+
return;
|
|
80
|
+
|
|
81
|
+
Tcl_SetMaxBlockTime(&es->max_block);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* Check proc: called after Tcl_WaitForEvent returns.
|
|
86
|
+
* Calls the consumer's C function pointer directly.
|
|
87
|
+
* No rb_funcall, no Ruby method dispatch — just a function pointer call.
|
|
88
|
+
*/
|
|
89
|
+
static void
|
|
90
|
+
es_check_proc(ClientData cd, int flags)
|
|
91
|
+
{
|
|
92
|
+
struct event_source *es = (struct event_source *)cd;
|
|
93
|
+
|
|
94
|
+
if (!(flags & TCL_FILE_EVENTS) && !(flags & TCL_ALL_EVENTS))
|
|
95
|
+
return;
|
|
96
|
+
|
|
97
|
+
es->check_fn(es->client_data);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ---------------------------------------------------------
|
|
101
|
+
* Ruby methods
|
|
102
|
+
* --------------------------------------------------------- */
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* Teek._register_event_source(check_fn_ptr, client_data_ptr, interval_ms) -> EventSource
|
|
106
|
+
*
|
|
107
|
+
* Registers a C function as a Tcl event source. The function will be called
|
|
108
|
+
* on every event loop iteration with no Ruby overhead.
|
|
109
|
+
*
|
|
110
|
+
* check_fn_ptr: Integer — address of a C function with signature void(*)(void*)
|
|
111
|
+
* client_data_ptr: Integer — address passed to check_fn (0 for NULL)
|
|
112
|
+
* interval_ms: Integer — max block time in ms (e.g. 16 for ~60fps)
|
|
113
|
+
*
|
|
114
|
+
* Returns an opaque EventSource object. Hold a reference to keep it alive.
|
|
115
|
+
* Call #unregister or let GC collect it to remove the event source.
|
|
116
|
+
*/
|
|
117
|
+
static VALUE
|
|
118
|
+
teek_register_event_source(VALUE self, VALUE fn_ptr, VALUE data_ptr, VALUE interval)
|
|
119
|
+
{
|
|
120
|
+
struct event_source *es;
|
|
121
|
+
VALUE obj;
|
|
122
|
+
int ms;
|
|
123
|
+
|
|
124
|
+
/* Validate */
|
|
125
|
+
event_source_check_fn fn = (event_source_check_fn)(uintptr_t)NUM2ULL(fn_ptr);
|
|
126
|
+
if (!fn) {
|
|
127
|
+
rb_raise(rb_eArgError, "check_fn_ptr must not be NULL");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ms = NUM2INT(interval);
|
|
131
|
+
if (ms < 1) ms = 1;
|
|
132
|
+
|
|
133
|
+
/* Allocate and populate */
|
|
134
|
+
obj = TypedData_Make_Struct(cEventSource, struct event_source, &event_source_type, es);
|
|
135
|
+
es->check_fn = fn;
|
|
136
|
+
es->client_data = (void *)(uintptr_t)NUM2ULL(data_ptr);
|
|
137
|
+
es->max_block.sec = ms / 1000;
|
|
138
|
+
es->max_block.usec = (ms % 1000) * 1000;
|
|
139
|
+
es->registered = 0;
|
|
140
|
+
|
|
141
|
+
/* Register with Tcl */
|
|
142
|
+
Tcl_CreateEventSource(es_setup_proc, es_check_proc, (ClientData)es);
|
|
143
|
+
es->registered = 1;
|
|
144
|
+
|
|
145
|
+
return obj;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* EventSource#unregister -> nil
|
|
150
|
+
*
|
|
151
|
+
* Explicitly removes the event source from Tcl's notifier.
|
|
152
|
+
* Safe to call multiple times.
|
|
153
|
+
*/
|
|
154
|
+
static VALUE
|
|
155
|
+
event_source_unregister(VALUE self)
|
|
156
|
+
{
|
|
157
|
+
struct event_source *es;
|
|
158
|
+
TypedData_Get_Struct(self, struct event_source, &event_source_type, es);
|
|
159
|
+
|
|
160
|
+
if (es->registered) {
|
|
161
|
+
Tcl_DeleteEventSource(es_setup_proc, es_check_proc, (ClientData)es);
|
|
162
|
+
es->registered = 0;
|
|
163
|
+
}
|
|
164
|
+
return Qnil;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/*
|
|
168
|
+
* EventSource#registered? -> true/false
|
|
169
|
+
*/
|
|
170
|
+
static VALUE
|
|
171
|
+
event_source_registered_p(VALUE self)
|
|
172
|
+
{
|
|
173
|
+
struct event_source *es;
|
|
174
|
+
TypedData_Get_Struct(self, struct event_source, &event_source_type, es);
|
|
175
|
+
return es->registered ? Qtrue : Qfalse;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ---------------------------------------------------------
|
|
179
|
+
* Init — called from Init_tcltklib
|
|
180
|
+
* --------------------------------------------------------- */
|
|
181
|
+
|
|
182
|
+
static VALUE cEventSource;
|
|
183
|
+
|
|
184
|
+
void
|
|
185
|
+
Init_tkeventsource(VALUE mTeek)
|
|
186
|
+
{
|
|
187
|
+
cEventSource = rb_define_class_under(mTeek, "EventSource", rb_cObject);
|
|
188
|
+
rb_undef_alloc_func(cEventSource); /* No Ruby-side new */
|
|
189
|
+
|
|
190
|
+
rb_define_method(cEventSource, "unregister", event_source_unregister, 0);
|
|
191
|
+
rb_define_method(cEventSource, "registered?", event_source_registered_p, 0);
|
|
192
|
+
|
|
193
|
+
rb_define_module_function(mTeek, "_register_event_source",
|
|
194
|
+
teek_register_event_source, 3);
|
|
195
|
+
}
|