glslkit 0.1.0.pre

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 13d9773aaab7e412f3a84693c9c5906310180722565fb870b64000b9bf8ea74a
4
+ data.tar.gz: 6dc9b85466232122da5d012bfe251564eee853c33b369c1cf2e73590bed9fd64
5
+ SHA512:
6
+ metadata.gz: fea6181044d5c985678d050bdeb582b002c63d435b54c569e489b0fbf7aeef4c36bfbbff72ef6742bb64974e56b2fc5a3fe1c6d493f8f7ec2b855e70e40b8552
7
+ data.tar.gz: 6a6d411e5e9f541f65742aac540c279e68b637b846a9c43f4f14cedd6a4d6c415e04a7a19332f7ba1cba93c8a06bfb1d93039fdb57a6ba8eaff54f30d22f1929
data/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ 初回リリース (v0.1)。
6
+
7
+ ### 追加
8
+
9
+ - `Glslkit::Preprocessor` — `#include`解決、`#pragma once`、`#version`/`#extension`の
10
+ 集約と衝突検出、`#line`ディレクティブ挿入、循環include検出
11
+ - `Glslkit::Resolvers::FileSystem` / `Glslkit::Resolvers::Hash`
12
+ - `Glslkit::Reflection` — attribute / uniform / uniform_block / output の抽出
13
+ - `Glslkit::Types` — GLSL型 → WebGL2 setter の対応表
14
+ - `Glslkit::Manifest` — `spec/schema/reflection-v1.json` (Draft 2020-12) に
15
+ 準拠したマニフェスト生成。ステージ間のuniform/uniform_block統合
16
+ - `Glslkit::Minifier` — コメント・空白のみの除去 (識別子リネームなし)
17
+ - `Glslkit::SourceMap` / `Glslkit::Source`
18
+ - `Glslkit::Validator` — E001〜E007 / W001〜W004 の検証ルール(ステージ間の
19
+ out/in不一致、cross-stage uniform型不一致、attribute/output location衝突、
20
+ ステージ内の重複宣言、`gl_`予約プレフィックス)。例外を投げず診断の配列を返す
21
+ - `Glslkit::Diagnostic` / `Glslkit::Program` — Validator/Manifest.build/
22
+ ライブリロードが共有する値オブジェクト
23
+ - `Glslkit::Bundle` — 前処理→Program化→検証→Manifest組み立てをまとめた
24
+ 中間表現。core/rails/wasmのどれにも属さず、`glslkit-webgl`の埋め込み生成
25
+ (`rake glslkit:embed`)と`glslkit-rails`のライブリロードエンドポイントの
26
+ 両方から使われる。ポーリング用のソースdigestも計算する
27
+ - `Glslkit::Digest` — stdlib `digest` への依存をこのファイルだけに閉じ込める
28
+ - `require "glslkit/runtime"` — マニフェストの読み込みとSourceMapの復元だけに
29
+ 必要な狭い入口。`glslkit-webgl`(ruby.wasm)側はこちらを使い、
30
+ 前処理・解析・digest計算のコードを読み込まない
31
+ - `Glslkit::Manifest` の `generator` フィールド — マニフェストが
32
+ `rake glslkit:embed`/`glslkit-rails`のどちらで生成されたかを来歴として残す
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kyubey1228
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # glslkit
2
+
3
+ GLSLソースを前処理・解析し、ビルド時に得たリフレクション情報(uniform / attribute
4
+ / output / uniform block)をJSONとして残すためのRuby gem。実行時依存はゼロ
5
+ (stdlibの `json` `strscan` `set` `digest` のみ)で、将来 [ruby.wasm](https://github.com/ruby/ruby.wasm)
6
+ 上のWebGLバインディングにそのまま組み込めることを狙っている。
7
+
8
+ Rails (Propshaft) との統合は別gem [`glslkit-rails`](../rails) が提供する。
9
+
10
+ ## インストール
11
+
12
+ ```ruby
13
+ gem "glslkit"
14
+ ```
15
+
16
+ ## 使い方
17
+
18
+ ```ruby
19
+ require "glslkit"
20
+
21
+ resolver = Glslkit::Resolvers::FileSystem.new(load_paths: ["app/shaders"])
22
+ source = Glslkit::Preprocessor.new(resolver: resolver).process("pbr.frag")
23
+
24
+ source.code # => 平坦化済みGLSL (String)。#include解決済み、#lineディレクティブ付き
25
+ source.reflection # => Glslkit::Reflection (attributes / uniforms / uniform_blocks / outputs)
26
+ source.source_map # => Glslkit::SourceMap (#lineのファイルインデックス → パス)
27
+ source.digest # => SHA256 hex (String)
28
+ ```
29
+
30
+ ### `#include` の解決
31
+
32
+ ```glsl
33
+ #include "common/math.glsl" // 相対探索 → load_pathsの順に探索
34
+ #include <glslkit/pbr.glsl> // load_pathsのみを探索(相対探索はしない)
35
+ ```
36
+
37
+ - 循環includeは `Glslkit::CircularIncludeError`
38
+ - `#pragma once` は2回目以降の展開をスキップ(デフォルトは毎回展開)
39
+ - `#version` は複数ファイルに散在していても1行目に1回だけ出力し、値が食い違えば
40
+ `Glslkit::VersionConflictError`
41
+ - `#if` / `#ifdef` / `#else` / `#endif` は評価せずそのまま出力する(内部の
42
+ `#include` は展開される。これはv0.1の既知の制限)
43
+
44
+ wasm側でファイルシステムの無い環境向けに `Glslkit::Resolvers::Hash` も同梱している:
45
+
46
+ ```ruby
47
+ resolver = Glslkit::Resolvers::Hash.new(
48
+ "pbr.frag" => "...",
49
+ "common/math.glsl" => "..."
50
+ )
51
+ ```
52
+
53
+ ### リフレクション抽出
54
+
55
+ ```ruby
56
+ source.reflection.attributes # => [#<Attribute name="a_position" type="vec3" location=0 array_size=1>, ...]
57
+ source.reflection.uniforms # => [#<Uniform name="u_mvp" type="mat4" setter="uniformMatrix4fv" matrix=true sampler=false ...>, ...]
58
+ source.reflection.uniform_blocks # => [#<UniformBlock name="Camera" layout="std140" binding=0>, ...]
59
+ source.reflection.outputs # => [#<Output name="fragColor" type="vec4" location=0>, ...]
60
+ ```
61
+
62
+ 型からWebGL2のsetterへの対応表は `Glslkit::Types` が唯一の正:
63
+
64
+ ```ruby
65
+ Glslkit::Types.setter_for("mat4") # => "uniformMatrix4fv"
66
+ Glslkit::Types.matrix?("mat4") # => true
67
+ Glslkit::Types.sampler?("sampler2D") # => true
68
+ ```
69
+
70
+ ### マニフェスト生成
71
+
72
+ vertex/fragmentの `Source` ペアから、`spec/schema/reflection-v1.json`
73
+ (Draft 2020-12 JSON Schema) に準拠したマニフェストを組み立てる:
74
+
75
+ ```ruby
76
+ manifest = Glslkit::Manifest.new(generated_at: Time.now.utc.iso8601)
77
+ manifest.add_program("pbr",
78
+ vertex: {path: "pbr.vert", source: vertex_source, url: "/assets/pbr-a1b2.vert"},
79
+ fragment: {path: "pbr.frag", source: fragment_source, url: "/assets/pbr-c3d4.frag"})
80
+
81
+ manifest.to_json
82
+ ```
83
+
84
+ 同名uniform/uniform_blockはステージ間で1エントリに統合され、型
85
+ (またはlayout/binding)が食い違えば `Glslkit::StageMismatchError` になる。
86
+
87
+ ### Minify
88
+
89
+ コメント・空白の除去のみを行う(識別子リネームはしない)。改行は一切削除しない
90
+ ため、`#line` ディレクティブの行番号や `Reflection` の抽出結果を壊さない:
91
+
92
+ ```ruby
93
+ Glslkit::Minifier.minify(source.code)
94
+ ```
95
+
96
+ ## エラークラス
97
+
98
+ `Glslkit::IncludeNotFound`, `Glslkit::PathTraversalError`,
99
+ `Glslkit::CircularIncludeError`, `Glslkit::VersionConflictError`,
100
+ `Glslkit::StageMismatchError`, `Glslkit::UnknownTypeError`
101
+
102
+ ## ライセンス
103
+
104
+ [MIT](LICENSE.txt)
data/glslkit.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/glslkit/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "glslkit"
7
+ spec.version = Glslkit::VERSION
8
+ spec.authors = ["kyubey1228"]
9
+ spec.email = ["kyuuka1228@gmail.com"]
10
+
11
+ spec.summary = "GLSL preprocessing and reflection toolkit"
12
+ spec.description = "Flattens #include directives in GLSL shaders and extracts " \
13
+ "uniform/attribute/output reflection data as JSON, with zero runtime " \
14
+ "dependencies so it can run under ruby.wasm."
15
+ spec.homepage = "https://github.com/kyubey1228/glslkit"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 3.1"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/main/core"
21
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/core/CHANGELOG.md"
22
+
23
+ spec.files = Dir.glob("lib/**/*.rb") + ["glslkit.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt"]
24
+ spec.require_paths = ["lib"]
25
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "preprocessor"
5
+ require_relative "program"
6
+ require_relative "validator"
7
+ require_relative "manifest"
8
+ require_relative "digest"
9
+
10
+ module Glslkit
11
+ # 1プログラム分(vertex+fragment)を「前処理→Program化→検証→Manifest組み立て」
12
+ # まで通す中間表現(M11b、SPEC-livereload.md §3.2)。core/rails/wasmのどれにも
13
+ # 属さない — `Glslkit::WebGL::Embed` はこれを `.rb` に整形するだけの薄い
14
+ # ラッパーになり、Rails側のライブリロードは同じ結果をJSONに整形して返す
15
+ # (シリアライズ形式だけが違う、というのがこのクラスを切り出した理由)。
16
+ #
17
+ # `.build` は例外を投げない。前処理(#include解決)が失敗した場合も、
18
+ # Validatorがエラーを返した場合も、結果は常に Result として返る。
19
+ # 例外にして呼び出し側に伝える/伝えないの判断は呼び出し側の役目
20
+ # (Embedは前者、Railsのライブリロードエンドポイントは後者になる想定)。
21
+ module Bundle
22
+ # Preprocessor#process とその内部で使うResolverが実際に投げうる例外を
23
+ # 尽くす。ここに列挙されていない例外(想定外のバグ等)は握り潰さず
24
+ # そのまま伝播させる。
25
+ PREPROCESS_ERRORS = [
26
+ IncludeNotFound,
27
+ CircularIncludeError,
28
+ VersionConflictError,
29
+ PathTraversalError
30
+ ].freeze
31
+
32
+ # kind: nil(成功) | :preprocess(#include解決の失敗) | :validation(Validatorのエラー)。
33
+ # kind == :validation でも vertex/fragment は入る(前処理自体は成功している)。
34
+ # manifest は kind.nil? のときだけ入る。
35
+ class Result
36
+ attr_reader :name, :kind, :source_digest, :vertex, :fragment, :manifest, :diagnostics, :error
37
+
38
+ def initialize(name:, source_digest:, kind: nil, vertex: nil, fragment: nil, manifest: nil, diagnostics: [], error: nil)
39
+ @name = name
40
+ @kind = kind
41
+ @source_digest = source_digest
42
+ @vertex = vertex
43
+ @fragment = fragment
44
+ @manifest = manifest
45
+ @diagnostics = diagnostics
46
+ @error = error
47
+ end
48
+
49
+ def ok?
50
+ kind.nil?
51
+ end
52
+ end
53
+
54
+ module_function
55
+
56
+ # resolver: #include解決に使うResolver(呼び出し側が用意する。FileSystem/Hash等)。
57
+ # vertex/fragment: resolverへ渡すエントリポイントのリクエスト文字列(相対パス)。
58
+ # urls: Manifestのstage.urlに載せる値。省略時はvertex/fragmentのパス自身を使う
59
+ # (Embedの用途。実URLが要るRails側は明示的に渡す)。
60
+ # known_files: ポーリング用digest(§3.2 決定1)を、前処理が失敗した回にも
61
+ # 計算するための「前回成功時のfiles」。呼び出し側が持ち回る
62
+ # (Bundle自身はステートレスに保つ)。
63
+ def build(resolver:, name:, vertex:, fragment:, line_directives: true, disabled_checks: [],
64
+ urls: {vertex: vertex, fragment: fragment}, known_files: [], now: Time.now)
65
+ preprocessor = Preprocessor.new(resolver: resolver, line_directives: line_directives)
66
+
67
+ begin
68
+ vertex_source = preprocessor.process(vertex)
69
+ fragment_source = preprocessor.process(fragment)
70
+ rescue *PREPROCESS_ERRORS => e
71
+ digest = source_digest(resolver, fallback_files(known_files, [vertex, fragment]))
72
+ return Result.new(name: name, kind: :preprocess, source_digest: digest, error: e)
73
+ end
74
+
75
+ program = Program.new(name: name, sources: {vertex: vertex_source, fragment: fragment_source})
76
+ validation = Validator.new(disabled: disabled_checks).validate(program)
77
+ digest = source_digest(resolver, (vertex_source.source_map.files + fragment_source.source_map.files).uniq)
78
+
79
+ unless validation.ok?
80
+ return Result.new(
81
+ name: name, kind: :validation, source_digest: digest,
82
+ vertex: vertex_source, fragment: fragment_source, diagnostics: validation.diagnostics
83
+ )
84
+ end
85
+
86
+ manifest = Manifest.build(programs: [program], urls: {name => urls}, now: now)
87
+ Result.new(
88
+ name: name, source_digest: digest, vertex: vertex_source, fragment: fragment_source,
89
+ manifest: manifest, diagnostics: validation.diagnostics
90
+ )
91
+ end
92
+
93
+ def fallback_files(known_files, entry_points)
94
+ (known_files.empty? ? entry_points : known_files + entry_points).uniq
95
+ end
96
+
97
+ # 依存する全ソースファイルの生バイト列(前処理後のコードではない)を
98
+ # パスでソートして "path\0content" で連結し、SHA256を取る(決定1)。
99
+ def source_digest(resolver, files)
100
+ entries = files.sort.filter_map do |path|
101
+ content = read_raw(resolver, path)
102
+ "#{path}\0#{content}" unless content.nil?
103
+ end
104
+ Digest.hexdigest(entries.join)
105
+ end
106
+
107
+ # digest計算専用の再読み込み。known_filesに含まれる「前回成功時のファイル」が
108
+ # その後削除されている等で読めなくなっていても、digest計算そのものは
109
+ # 落とさない(ポーリングを壊さないことを優先する。digestは変化を検知できれば
110
+ # 十分で、読めなかった1ファイルの厳密な反映までは求めない)。
111
+ def read_raw(resolver, path)
112
+ _, content = resolver.read(path, from: nil)
113
+ content
114
+ rescue
115
+ nil
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glslkit
4
+ # 検証結果1件分(§8.2)。file/lineの解決はValidatorの役目であり、
5
+ # DiagnosticやReflection自身は関与しない。
6
+ Diagnostic = Struct.new(:severity, :code, :message, :program, :stage, :name, :file, :line, keyword_init: true) do
7
+ def error?
8
+ severity == :error
9
+ end
10
+
11
+ def warning?
12
+ severity == :warning
13
+ end
14
+
15
+ def to_s
16
+ "[#{code}] #{location}: #{message}"
17
+ end
18
+
19
+ private
20
+
21
+ def location
22
+ return "#{file}:#{line}" if file
23
+
24
+ stage ? "#{program}(#{stage})" : program.to_s
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Glslkit
6
+ # stdlibの `digest` を要求するのはこのファイルだけにする(M8g)。
7
+ # `glslkit/runtime`(wasm向けの狭い入口)はこのファイルをrequireしないため、
8
+ # マニフェストの読み込み・SourceMapの復元だけならdigestはロードされない。
9
+ module Digest
10
+ module_function
11
+
12
+ # 定数探索はこのモジュール自身より先にこのレキシカルスコープを見るため、
13
+ # 曖昧さを避けて常にトップレベルの `::Digest` を明示する。
14
+ def hexdigest(string)
15
+ ::Digest::SHA256.hexdigest(string)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glslkit
4
+ class UnknownTypeError < StandardError; end
5
+ class IncludeNotFound < StandardError; end
6
+ class PathTraversalError < StandardError; end
7
+ class CircularIncludeError < StandardError; end
8
+ class VersionConflictError < StandardError; end
9
+ class StageMismatchError < StandardError; end
10
+ # M11a: glslkit-railsのassets:precompileフックが、Validatorのエラーで
11
+ # ビルドを止めるときに投げる(SPEC.md §8.6)。coreはこれを自分では投げない
12
+ # (Rails側からしか使われないが、例外クラス自体は名前空間の都合でcoreに置く)。
13
+ class ValidationFailed < StandardError; end
14
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "errors"
5
+ require_relative "types"
6
+ require_relative "version"
7
+
8
+ module Glslkit
9
+ # プログラムごとのvertex+fragment Sourceペアから reflection-v1.json の
10
+ # マニフェスト(spec/schema/reflection-v1.json)を組み立てる。各stageの
11
+ # `url`は呼び出し側から渡される不透明な文字列(例: Propshaftの
12
+ # フィンガープリント付きアセットパス)である — glslkit自身のdigestとは
13
+ # 無関係で、digestから導出することはなく、渡された値をそのまま格納する
14
+ # だけである。
15
+ class Manifest
16
+ class UnsupportedVersionError < StandardError; end
17
+
18
+ # JSON文字列(またはJSON.parse済みのHash)からManifestを復元する(M8g)。
19
+ # `glslkit/runtime`(digestをロードしない狭い入口)から使われる想定のため、
20
+ # ここではdigestに一切触れない。検証は行わない — 壊れた入力は
21
+ # KeyError/JSON::ParserError等をそのまま投げる。
22
+ def self.parse(json_string_or_hash)
23
+ hash = json_string_or_hash.is_a?(String) ? JSON.parse(json_string_or_hash) : json_string_or_hash
24
+ version = hash.fetch("schema_version")
25
+ raise UnsupportedVersionError, "unsupported schema_version: #{version.inspect}" unless version == 1
26
+
27
+ new(generated_at: hash.fetch("generated_at"), programs: hash.fetch("programs"), generator: hash["generator"])
28
+ end
29
+
30
+ # Glslkit::Program (§8.3) の配列からマニフェストのHashを直接組み立てる。
31
+ # Validatorと入力を共有できるようにする入口で、内部的には
32
+ # add_program に委譲するだけ(既存のマージロジックには手を入れない)。
33
+ #
34
+ # vertex/fragmentが両方揃っていないProgramは(共有partial等の可能性が
35
+ # あるため)黙ってスキップする — SPEC.md §2.4/§3の「プログラムは
36
+ # 両ステージ必須」という決定と、ManifestBuilder(rails側)が既に
37
+ # 行っているのと同じ扱い。
38
+ #
39
+ # 検証(Validator)は必ずこれより先に呼ぶこと(§8.6)。ここは今まで通り
40
+ # StageMismatchErrorを投げる経路のままなので、検証を経ていない入力を
41
+ # 渡すと診断を得る前に例外で落ちる。
42
+ def self.build(programs:, urls: {}, now: Time.now)
43
+ # digest(§8.9)と同じ扱い: `.iso8601` はstdlibの"time"の拡張なので、
44
+ # 実際に使う`.build`経路でだけ遅延require する。`glslkit/runtime`
45
+ # (`.parse`/`.to_h`のみ)はこのメソッドを経由しないため影響しない。
46
+ require "time"
47
+ manifest = new(generated_at: now.utc.iso8601, generator: "glslkit/#{Glslkit::VERSION}")
48
+ programs.each do |program|
49
+ next unless program.sources.key?(:vertex) && program.sources.key?(:fragment)
50
+
51
+ manifest.add_program(
52
+ program.name,
53
+ vertex: stage_input(program, :vertex, urls),
54
+ fragment: stage_input(program, :fragment, urls)
55
+ )
56
+ end
57
+ manifest.to_h
58
+ end
59
+
60
+ def self.stage_input(program, stage, urls)
61
+ ext = (stage == :vertex) ? "vert" : "frag"
62
+ {path: "#{program.name}.#{ext}", source: program.sources.fetch(stage), url: urls.dig(program.name, stage)}
63
+ end
64
+ private_class_method :stage_input
65
+
66
+ def initialize(generated_at:, programs: {}, generator: nil)
67
+ @generated_at = generated_at
68
+ @programs = programs
69
+ @generator = generator
70
+ end
71
+
72
+ def add_program(name, vertex:, fragment:)
73
+ @programs[name.to_s] = build_program(vertex: vertex, fragment: fragment)
74
+ self
75
+ end
76
+
77
+ # `generator`(M10e)は「このManifestを実際に組み立てたのは何か」を表す
78
+ # 来歴情報。`build`が呼ばれたときにだけそのバージョンで確定させ、`to_h`が
79
+ # 呼ばれるたびに現在実行中のバージョンで上書きしたりはしない —
80
+ # `parse`で読み込んだ古いバージョンの生成物の来歴を破壊しないため。
81
+ # 未設定(手書きのManifestや、generatorを持たない古い生成物)なら
82
+ # キー自体を出さない。`reflection-v1.json`でoptional(型はstring)なので
83
+ # nullを出すとスキーマ違反になる。
84
+ def to_h
85
+ {
86
+ "schema_version" => 1,
87
+ "generated_at" => @generated_at,
88
+ "generator" => @generator,
89
+ "programs" => @programs
90
+ }.compact
91
+ end
92
+
93
+ def to_json(*args)
94
+ JSON.generate(to_h, *args)
95
+ end
96
+
97
+ private
98
+
99
+ def build_program(vertex:, fragment:)
100
+ vertex_reflection = vertex.fetch(:source).reflection
101
+ fragment_reflection = fragment.fetch(:source).reflection
102
+
103
+ {
104
+ "digest" => program_digest(vertex.fetch(:source).digest, fragment.fetch(:source).digest),
105
+ "stages" => {
106
+ "vertex" => stage_hash(vertex),
107
+ "fragment" => stage_hash(fragment)
108
+ },
109
+ "attributes" => vertex_reflection.attributes.map { |a| attribute_hash(a) },
110
+ "uniforms" => merge_uniforms(vertex_reflection.uniforms, fragment_reflection.uniforms),
111
+ "uniform_blocks" => merge_uniform_blocks(vertex_reflection.uniform_blocks, fragment_reflection.uniform_blocks),
112
+ "outputs" => fragment_reflection.outputs.map { |o| output_hash(o) }
113
+ }
114
+ end
115
+
116
+ # digestをrequireするのは実際にdigestを計算する経路(build/add_program)
117
+ # だけにするため、ここで遅延require する(M8g)。`glslkit/runtime` 経由で
118
+ # `parse`/`to_h` だけを使う限りこのメソッドは呼ばれず、digestはロードされない。
119
+ def program_digest(vertex_digest, fragment_digest)
120
+ require_relative "digest"
121
+ Digest.hexdigest("#{vertex_digest}:#{fragment_digest}")
122
+ end
123
+
124
+ def stage_hash(stage)
125
+ {"path" => stage.fetch(:path), "digest" => stage.fetch(:source).digest, "url" => stage.fetch(:url)}
126
+ end
127
+
128
+ def attribute_hash(attribute)
129
+ {"name" => attribute.name, "type" => attribute.type, "location" => attribute.location,
130
+ "array_size" => attribute.array_size}
131
+ end
132
+
133
+ def output_hash(output)
134
+ {"name" => output.name, "type" => output.type, "location" => output.location}
135
+ end
136
+
137
+ def merge_uniforms(vertex_uniforms, fragment_uniforms)
138
+ group_by_name(vertex: vertex_uniforms, fragment: fragment_uniforms).map do |name, by_stage|
139
+ first_stage, first_entry = by_stage.first
140
+ by_stage.each do |stage, entry|
141
+ next if entry.type == first_entry.type
142
+
143
+ raise StageMismatchError,
144
+ "uniform #{name.inspect} is #{first_entry.type.inspect} in #{first_stage} " \
145
+ "but #{entry.type.inspect} in #{stage}"
146
+ end
147
+
148
+ components = Types.components_for(first_entry.type)
149
+ {
150
+ "name" => name,
151
+ "type" => first_entry.type,
152
+ "array_size" => first_entry.array_size,
153
+ "setter" => first_entry.setter,
154
+ "matrix" => first_entry.matrix,
155
+ "sampler" => first_entry.sampler,
156
+ "components" => components,
157
+ "element_count" => components * first_entry.array_size,
158
+ "stages" => by_stage.keys.map(&:to_s)
159
+ }
160
+ end
161
+ end
162
+
163
+ def merge_uniform_blocks(vertex_blocks, fragment_blocks)
164
+ group_by_name(vertex: vertex_blocks, fragment: fragment_blocks).map do |name, by_stage|
165
+ first_stage, first_entry = by_stage.first
166
+ by_stage.each do |stage, entry|
167
+ next if entry.layout == first_entry.layout && entry.binding == first_entry.binding
168
+
169
+ raise StageMismatchError, "uniform block #{name.inspect} differs between #{first_stage} and #{stage}"
170
+ end
171
+
172
+ {
173
+ "name" => name,
174
+ "layout" => first_entry.layout,
175
+ "binding" => first_entry.binding,
176
+ "stages" => by_stage.keys.map(&:to_s)
177
+ }
178
+ end
179
+ end
180
+
181
+ # #nameを持つエントリの {vertex: [...], fragment: [...]} を受け取り、
182
+ # 初出順(vertexのエントリが先、fragmentのみの名前はその後)を保った
183
+ # name => {stage => entry} の順序付きHashを返す。
184
+ #
185
+ # 同一ステージのentries内に同名が2件以上あった場合、後のものが前のものを
186
+ # 静かに上書きする(`[stage] = entry`)。これは**入力がValidator(§8.3の
187
+ # E006)で検証済みで、同一ステージ内の重複が存在しないこと**を前提として
188
+ # 許容している。Validatorを経ていない入力でこのメソッドを単体利用すると、
189
+ # 重複が黙って握り潰されるので注意。
190
+ def group_by_name(stage_entries)
191
+ grouped = {}
192
+ stage_entries.each do |stage, entries|
193
+ entries.each { |entry| (grouped[entry.name] ||= {})[stage] = entry }
194
+ end
195
+ grouped
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glslkit
4
+ # コメントと不要な空白のみを除去する — 識別子のリネームも行数のマージも
5
+ # しない。プリプロセッサディレクティブ(#version, #line, #ifdefなど)は
6
+ # それぞれ独立した1物理行でなければならないため、改行は一切削除しない:
7
+ # 入力の各行は必ず出力のちょうど1行に対応するので、コード中に既にある
8
+ # #lineディレクティブの行番号は正しいまま保たれ、Reflectionの抽出結果も
9
+ # 変化しない。
10
+ module Minifier
11
+ COMMENT_PATTERN = %r{//[^\n]*|/\*.*?\*/}m
12
+
13
+ module_function
14
+
15
+ def minify(code)
16
+ strip_comments(code).each_line.map { |line| minify_line(line) }.join
17
+ end
18
+
19
+ def strip_comments(code)
20
+ code.gsub(COMMENT_PATTERN) do |match|
21
+ newlines = match.count("\n")
22
+ # 改行を含まないコメントでも、区切りは残しておく必要がある。
23
+ # 例えば `vec3/* x */foo` が `vec3foo` になってしまわないように。
24
+ newlines.positive? ? ("\n" * newlines) : " "
25
+ end
26
+ end
27
+
28
+ def minify_line(line)
29
+ ending = line.end_with?("\n") ? "\n" : ""
30
+ "#{line.chomp.strip.gsub(/[ \t]+/, " ")}#{ending}"
31
+ end
32
+ end
33
+ end