Source browser

scripts/generate_api.py

Read the complete file below or download its original bytes. Line numbers provide stable links to a declaration or processing step.

File fingerprint
1#!/usr/bin/env python3
2"""Render public runtime declarations as a searchable HTML reference.
3
4The reference includes syntax-highlighted signatures and linked header source.
5"""
6
7from __future__ import annotations  # Keep type hints usable on older supported Python versions.
8
9import argparse  # Read command-line options and show help.
10import html  # Escape source text before placing it in HTML.
11from pathlib import Path  # Build paths and read or write files.
12import re  # Find declarations, comments and diagnostic lines.
13from highlight_docs import add_stylesheet, highlight_code, highlight_lines  # Render static syntax colors and preserve source text.
14
15
16ROOT = Path(__file__).resolve().parent.parent  # Find the checkout containing the maintained source files.
17GROUPS = (  # List the API sections in their displayed order.
18    "Build integration and shader exports",  # Group device setup and shader export helpers.
19    "Resources and synchronization",  # Group shared buffers, images and transfer methods.
20    "Kernels and dispatch",  # Group kernel interfaces and dispatch dimensions.
21    "Pipelines and dynamic libraries",  # Group linked stages and kernel factories.
22    "Asynchronous execution",  # Group fence handles and CPU work queues.
23    "Headless EGL backend",  # Group optional context and headless device creation.
24    "GLSL math",  # Group portable vector, matrix and numeric functions.
25    "C++ adapter declaration macros",  # Group the shader interface declaration macros.
26    "Other documented declarations",  # Keep remaining public helpers discoverable.
27)  # Keep this display order when collecting rendered API sections.
28
29
30def clean_comment(raw: str) -> str:  # Remove C++ comment prefixes before rendering their text.
31    """Strip comment decoration while preserving paragraph boundaries."""
32    raw = re.sub(r"^/\*\*|\*/$", "", raw.strip())  # Remove the outer comment markers.
33    return "\n".join(  # Return comment text with its paragraph boundaries preserved.
34        re.sub(r"^\s*(?:\*\s?|///\s?)", "", line).rstrip() for line in raw.splitlines()  # Remove decoration from each comment line.
35    ).strip()  # Remove whitespace around the complete cleaned docstring.
36
37
38def declaration_after(source: str, end: int) -> str:  # Read the C++ declaration immediately following a docstring.
39    """Read a declaration, omitting inline bodies and multiline macro bodies."""
40    remainder = source[end:].lstrip()  # Read the text following this declaration or field.
41    if not remainder or remainder.startswith(("/**", "///")):  # Select comments attached to a declaration.
42        return ""  # Return an empty signature for this unattached comment.
43    if remainder.startswith("#define"):  # Show the macro name and parameters as its signature.
44        macro = re.match(r"#define\s+\w+(?:\([^)]*\))?", remainder)  # Recognize a generated macro or declaration name.
45        return macro.group() if macro else remainder.splitlines()[0]  # Keep the macro name and parameters, or its first line as a fallback.
46    if re.match(r"enum(?:\s+class)?\s+\w+", remainder):  # Keep the complete value list when the documented declaration is an enum.
47        end = remainder.find(";")  # Locate the declaration or directive end.
48        if 0 <= end < 800:  # Accept a nearby enum terminator while avoiding an unrelated later declaration.
49            return remainder[: end + 1]  # Include the enum declaration through its final semicolon.
50    lines = []  # Collect declaration signature lines.
51    for line in remainder.splitlines()[:14]:  # Limit the search to fourteen lines after the docstring.
52        line = line.split("//", 1)[0].rstrip()  # Remove inline C++ comments from the displayed signature.
53        found = re.search(r"[;{]", line)  # Locate the first declaration terminator on this line.
54        if found:  # Stop at a declaration terminator or the opening of an inline function body.
55            lines.append(line[: found.start()] + (";" if line[found.start()] == ";" else ""))  # Keep this line in the displayed C++ signature.
56            break  # Stop once the declaration boundary has been found.
57        if line.strip():  # Retain nonempty signature lines before that boundary.
58            lines.append(line)  # Keep this line in the displayed C++ signature.
59    return "\n".join(lines).strip()  # Return the collected signature with its original line breaks.
60
61
62def choose_group(declaration: str, comment: str, position: int, source: str) -> str:  # Place each API entry in a useful documentation section.
63    """Group public types and functions, with a GLSL namespace fallback."""
64    combined = declaration + " " + comment  # Search both the signature and its explanation.
65    if re.match(r"#define\s+SHDR_VERSION(?:_|\b)", declaration):  # Keep release identification with the basic build and setup helpers.
66        return GROUPS[0]  # Show version macros before the shader declaration interface.
67    if declaration.startswith("#define"):  # Keep declaration macros together even when their prose mentions resources.
68        return GROUPS[7]  # Use the adapter macro section.
69    if re.search(r"\b(headless_context|headless_device|backend_unavailable)\b", combined):  # Recognize the optional EGL public types by name.
70        return GROUPS[5]  # Use the EGL section.
71    async_start = source.find("// ---- async.hpp ----")  # Locate the asynchronous runtime section.
72    egl_start = source.find("// ---- egl.hpp ----")  # Locate the optional EGL section.
73    macros_start = source.find("// ---- macros.hpp ----")  # Locate the adapter declaration section.
74    if egl_start >= 0 and position >= egl_start and (macros_start < 0 or position < macros_start):  # Place declarations within the EGL source section in its own group.
75        return GROUPS[5]  # Use the EGL section.
76    if async_start >= 0 and position >= async_start and (egl_start < 0 or position < egl_start):  # Place declarations within the asynchronous source section in its own group.
77        return GROUPS[4]  # Use the asynchronous section.
78    if re.search(  # Recognize pipeline and registered factory declarations.
79        r"\b(pipeline|module|plugin|kernel_export|shared_library|load_kernel|make_kernel|register_kernel)\b",  # Recognize pipeline composition and dynamic factory names.
80        combined,  # Search the signature together with its explanation.
81    ):  # Use composition and factory names to choose the pipeline group.
82        return GROUPS[3]  # Use the pipeline section.
83    if re.search(r"\b(stage|dispatch|backend|kernel)\b", declaration):  # Recognize dispatch dimensions, stages and kernel interfaces.
84        return GROUPS[2]  # Use the kernel section.
85    if re.search(  # Recognize resource types and transfer methods.
86        r"\b(buffer|image2d|access|upload|download|from_gl|host_dirty|gpu_dirty)\b", combined  # Match the supported names in this API group.
87    ):  # Group buffer and image synchronization APIs together.
88        return GROUPS[1]  # Use the resource section.
89    if re.search(  # Recognize basic setup, source export and error declarations.
90        r"\b(device|profile|glsl_source|glsl_prelude|read_file|error|resolver|extent|ceil_div)\b",  # Recognize device setup, shader export and basic runtime names.
91        combined,  # Search the signature together with its explanation.
92    ):  # Group setup helpers with backend configuration.
93        return GROUPS[0]  # Use the build and device section.
94    glsl_start = source.find("namespace glsl")  # Locate the math namespace in the header.
95    runtime_start = source.find("namespace shdr", glsl_start + 1)  # Find where runtime declarations follow the math section.
96    if (  # Use namespace position to identify remaining math declarations.
97        glsl_start >= 0  # Require that the math namespace was found.
98        and position >= glsl_start  # Check that this declaration follows the math namespace opening.
99        and (runtime_start < 0 or position < runtime_start)  # Keep math entries before the runtime namespace begins.
100    ):  # Keep declarations inside the math namespace in the math group.
101        return GROUPS[6]  # Use the math section.
102    return GROUPS[8]  # Use the remaining public declarations section.
103
104
105def family_symbols(body: str) -> list[str]:  # List functions covered by one math-family docstring.
106    """Collect names from public declarations and the header's math generator calls."""
107    body = re.sub(r"/\*[\s\S]*?\*/|//[^\n]*", "", body)  # Remove comments before scanning math declarations.
108    names = []  # Collect the public names found in this family.
109    depth = 0  # Track nested bodies so only public names are listed.
110    in_macro = False  # Track whether this line continues a macro definition.
111    for line in body.splitlines():  # Read each declaration and generator call in source order.
112        stripped = line.strip()  # Inspect the current line after trimming indentation.
113        if in_macro or stripped.startswith("#define"):  # Select public declarations surrounding the macro definitions.
114            in_macro = stripped.endswith("\\")  # Track whether this line continues a macro definition.
115            continue  # Skip this item and inspect the next one.
116        macro = re.match(r"SHDR_DETAIL_\w+\(([^,)]+)", stripped)  # Recognize a generated macro or declaration name.
117        if macro and depth == 0:  # Read a public symbol passed to a top-level math generator.
118            name = macro.group(1).strip()  # Trim surrounding whitespace from the symbol name.
119            if re.fullmatch(r"[A-Za-z_]\w*", name):  # Accept ordinary identifier names from a generator call.
120                names.append(name)  # Keep this public function or operator name.
121            elif re.fullmatch(r"[+*/%&|^<>=!-]+", name):  # Handle this source form.
122                names.append("operator" + name)  # Recognize operator tokens supplied to the math generator.
123        elif depth == 0 and not stripped.startswith("#"):  # Handle this source form.
124            candidates = re.findall(r"(?<![:\w])([A-Za-z_]\w*)\s*\(", line)  # Scan ordinary top-level declarations outside preprocessor directives.
125            candidates = [  # Find possible function names and remove language keywords.
126                name  # Keep this candidate function name.
127                for name in candidates  # Inspect the names matched on this declaration line.
128                if name  # Keep candidate public function names.
129                not in {"if", "for", "while", "switch", "static_assert", "decltype", "sizeof"}  # Remove language constructs that resemble function calls.
130            ]  # Remove language keywords from the list of possible function names.
131            if candidates:  # Keep the first public function name on this declaration line.
132                names.append(candidates[0])  # Keep this public function or operator name.
133        depth += line.count("{") - line.count("}")  # Track nested bodies so only public names are listed.
134    return list(dict.fromkeys(names))  # Return unique public names in source order.
135
136
137def documented_declarations(source: str) -> list[dict[str, str | int]]:  # Collect public declarations and their source locations.
138    """Collect header docstrings, declarations, groups, and source line numbers."""
139    expression = re.compile(r"/\*\*[\s\S]*?\*/|(?:^[ \t]*///[^\n]*(?:\n|$))+", re.MULTILINE)  # Match block docstrings and triple-slash comments.
140    entries = []  # Collect the documented public declarations.
141    for match in expression.finditer(source):  # Visit each matched public docstring.
142        comment = clean_comment(match.group())  # Read the cleaned C++ comment text.
143        if not comment or "@internal" in comment or "\\internal" in comment:  # Skip empty comments and entries explicitly marked internal.
144            continue  # Continue to the next public docstring.
145        if re.fullmatch(r"(?:@|\\)[{}]\s*", comment):  # Recognize structural Doxygen group markers.
146            continue  # Continue to the next documented declaration.
147        family = re.search(r"(?:@|\\)name\s+([^\n]+)", comment)  # Recognize a named group of math functions.
148        if family:  # Treat a named Doxygen family as one math API entry.
149            closing = re.search(r"/\*\*\s*(?:@|\\)}\s*\*/", source[match.end() :])  # Locate the end of the documented math family.
150            family_end = match.end() + closing.start() if closing else match.end()  # Limit the symbol scan to this family.
151            names = family_symbols(source[match.end() : family_end])  # Collect the public names found in this family.
152            declaration = family.group(1) + ("\n" + ", ".join(names) if names else "")  # Read the displayed signature for this API entry.
153            entries.append(  # Store the public signature, explanation, group and source line.
154                {  # Store the location needed to link this entry to its header.
155                    "comment": comment,  # Keep the entry's explanatory text.
156                    "declaration": declaration,  # Keep the displayed C++ signature.
157                    "group": GROUPS[6],  # Choose the API section for this entry.
158                    "line": source.count("\n", 0, match.start()) + 1,  # Link this entry to its header source line.
159                }  # Complete this family entry with its source location.
160            )  # Add the entire documented family as one API card.
161            continue  # Continue after publishing this complete family entry.
162        declaration = declaration_after(source, match.end())  # Read the displayed signature for this API entry.
163        if not declaration or re.match(r"(?:#(?:endif|if|include)|namespace\b)", declaration):  # Skip unattached comments and namespace or conditional directives.
164            continue  # Continue to the next attached public declaration.
165        entries.append(  # Store the public signature, explanation, group and source line.
166            {  # Store the location needed to link this entry to its header.
167                "comment": comment,  # Keep the entry's explanatory text.
168                "declaration": declaration,  # Keep the displayed C++ signature.
169                "group": choose_group(declaration, comment, match.start(), source),  # Choose the API section for this entry.
170                "line": source.count("\n", 0, match.start()) + 1,  # Link this entry to its header source line.
171            }  # Complete the ordinary declaration entry.
172        )  # Add this declaration to the exported API entries.
173    return entries  # Return all collected public API entries.
174
175
176def render_comment(comment: str) -> str:  # Convert the supported Doxygen tags to escaped HTML.
177    """Render a small Doxygen tag subset, escaping every source-content string."""
178    comment = re.sub(r"(?:@|\\)brief\s+", "", comment)  # Remove the brief tag while retaining its explanation.
179    comment = re.sub(r"(?:@|\\)(?:ingroup|defgroup)\s+[^\n]+", "", comment)  # Drop group-control tags from the displayed prose.
180    comment = re.sub(r"(?:@|\\)name\s+[^\n]+", "", comment)  # Keep the family title in its signature.
181    comment = re.sub(r"(?:@|\\)[{}]", "", comment)  # Remove Doxygen group-boundary markers from the prose.
182    paragraphs = []  # Collect escaped HTML paragraphs.
183    for paragraph in re.split(r"\n\s*\n", comment.strip()):  # Render each comment paragraph separately.
184        escaped = html.escape(paragraph)  # Escape text before adding supported formatting tags.
185        escaped = re.sub(  # Turn a parameter tag into a label while retaining its explanation.
186            r"(?:@|\\)param(?:\[[^]]+\])?\s+(\w+)\s+",  # Match an optional direction qualifier and the parameter name.
187            r"<strong>Parameter <code>\1</code>:</strong> ",  # Show each parameter name beside its explanation.
188            escaped,  # Keep the escaped explanation in the displayed paragraph.
189        )  # Finish rendering this parameter label.
190        for tags, label in (  # Handle each supported Doxygen label.
191            ("return|returns", "Returns"),  # Label documented return values.
192            ("throw|throws|exception", "Throws"),  # Label documented exceptions.
193            ("note", "Note"),  # Label additional usage notes.
194            ("warning", "Requirement"),  # Label required preconditions.
195        ):  # Apply each label to the matching Doxygen tag.
196            escaped = re.sub(  # Replace the current tag with a readable HTML label.
197                r"(?:@|\\)(?:" + tags + r")\s+", "<strong>" + label + ":</strong> ", escaped  # Replace this documented tag and preserve surrounding text.
198            )  # Keep the rest of this escaped paragraph unchanged.
199        escaped = re.sub(r"`([^`]+)`", r"<code>\1</code>", escaped)  # Render backtick-delimited text as inline code.
200        paragraphs.append("<p>" + escaped.replace("\n", "<br>") + "</p>")  # Keep this escaped paragraph in its original order.
201    return "\n".join(paragraphs)  # Return the joined HTML paragraphs.
202
203
204def export(header: Path, output: Path) -> int:  # Build the searchable API page from the public header.
205    """Write deterministic HTML and return the number of exported declarations."""
206    source = header.read_text(encoding="utf-8")  # Read the complete amalgamated header as UTF-8 text.
207    version_match = re.search(r'^\s*#define\s+SHDR_VERSION\s+"([^"]+)"', source, re.MULTILINE)  # Read the release label from the actual exported header.
208    if not version_match:  # Require the header to identify the documented release.
209        raise SystemExit("SHDR_VERSION is missing from " + str(header))  # Avoid publishing an unlabeled API reference.
210    version = version_match.group(1)  # Keep this release label separate from all API source text.
211    entries = documented_declarations(source)  # Collect the documented public declarations.
212    if not entries:  # Require public API comments in the selected header.
213        raise SystemExit("No API docstrings found in " + str(header))  # Identify the header that needs public API comments.
214    navigation = []  # Collect links to the visible API sections.
215    sections = []  # Collect the rendered API groups.
216    for index, group in enumerate(GROUPS):  # Render each API group in its displayed order.
217        selected = [entry for entry in entries if entry["group"] == group]  # Visit groups in the order shown by the navigation.
218        if not selected:  # Select groups containing public entries.
219            continue  # Skip this item and inspect the next one.
220        anchor = "group-" + str(index)  # Continue to the next populated group.
221        navigation.append(f'<a href="#{anchor}">{html.escape(group)}</a>')  # Link the displayed section from the page navigation.
222        articles = []  # Collect each declaration card in this section.
223        for entry in selected:  # Render each declaration in this group.
224            signature = str(entry["declaration"])  # Read this entry's displayed C++ declaration.
225            query = re.sub(r"\s+", " ", signature + " " + str(entry["comment"])).lower()  # Normalize the entry text for case-insensitive search.
226            articles.append(  # Add one escaped declaration card to this API section.
227                '<article class="entry" data-search="' + html.escape(query, quote=True) + '">\n'  # Keep source text escaped in the HTML card.
228                '<pre><code class="syntax-highlight">'  # Keep each highlighted declaration in its own readable code block.
229                + highlight_code(signature, "cpp")  # Color the C++ declaration while preserving its exact displayed text.
230                + "</code></pre>\n"  # Close the displayed signature before its explanation.
231                + render_comment(str(entry["comment"]))  # Show the formatted docstring below its declaration.
232                + "\n"  # Separate the rendered pieces with readable line breaks.
233                + f'<a class="source" href="#L{entry["line"]}">shdr.h, line {entry["line"]}</a>\n</article>'  # Link the API card to its declaration in the source browser.
234            )  # Finish this part of the API page.
235        sections.append(  # Add the rendered group to the complete page.
236            f'<section id="{anchor}"><h2>{html.escape(group)}</h2>\n'  # Start the section with a stable anchor and escaped group title.
237            + "\n".join(articles)  # Place this group's declaration cards below its title.
238            + "\n</section>"  # Close the section after its final declaration card.
239        )  # Finish this part of the API page.
240    document = (  # Assemble the complete standalone HTML page.
241        """<!doctype html>
242<html lang="en"> <!-- Declare the language used by the documentation. -->
243<head> <!-- Keep page metadata and styles separate from its content. -->
244<meta charset="utf-8"> <!-- Display source names and math symbols as UTF-8. -->
245<meta name="viewport" content="width=device-width,initial-scale=1"> <!-- Fit small screens. -->
246<title>shdr __SHDR_DOC_VERSION__ API reference</title> <!-- Name the browser tab and release. -->
247<style> /* Keep the API page usable without a stylesheet download. */
248:root { /* Set colors for the default light theme. */
249    color-scheme: light dark; /* Allow matching browser controls in either theme. */
250    --bg: #fbfcff; /* Use a light page background. */
251    --fg: #1f2937; /* Use dark text with readable contrast. */
252    --border: #d7dfe9; /* Separate API entries with a subtle rule. */
253    --accent: #1d4ed8; /* Give links a recognizable blue color. */
254    --code: #eef2f8; /* Set code apart from prose. */
255} /* Finish the light-theme values. */
256@media (prefers-color-scheme: dark) { /* Follow the reader's system theme. */
257    :root { /* Override only the theme colors. */
258        --bg: #111827; /* Use a dark page background. */
259        --fg: #e5e7eb; /* Keep text readable on the dark background. */
260        --border: #374151; /* Keep entry rules visible without high contrast. */
261        --accent: #93c5fd; /* Lighten links for the dark background. */
262        --code: #1f2937; /* Keep code panels distinct from the page. */
263    } /* Finish the dark color overrides. */
264} /* Keep layout rules shared by both themes. */
265* { box-sizing: border-box; } /* Include padding in element widths. */
266body { /* Use a readable system font and remove default page margins. */
267    margin: 0; /* Let the main content control spacing. */
268    background: var(--bg); /* Follow the selected background color. */
269    color: var(--fg); /* Follow the selected foreground color. */
270    font: 16px/1.55 system-ui, sans-serif; /* Give prose a comfortable line height. */
271} /* Finish the page defaults. */
272main { max-width: 1000px; margin: 0 auto; padding: 32px 24px 64px; } /* Center the readable column. */
273a { color: var(--accent); } /* Make links distinct in both themes. */
274h1 { line-height: 1.1; } /* Keep the page title compact. */
275h2 { margin-top: 44px; } /* Separate each API section. */
276nav { display: flex; gap: 12px 20px; flex-wrap: wrap; margin: 24px 0; } /* Wrap section links. */
277code { font: 0.92em ui-monospace, SFMono-Regular, Consolas, monospace; } /* Align source characters. */
278pre { overflow: auto; padding: 16px; background: var(--code); border-radius: 8px; } /* Frame code blocks. */
279pre code { white-space: pre-wrap; } /* Wrap long declarations on narrow screens. */
280.entry { border-top: 1px solid var(--border); padding: 8px 0 18px; } /* Separate declaration cards. */
281.entry p { margin: 10px 0; } /* Keep explanations close to their signatures. */
282.source { font-size: 13px; } /* Keep source-line links smaller than prose. */
283input { /* Make the search control match the page. */
284    width: 100%; /* Span the documentation column. */
285    padding: 12px; /* Keep the text easy to edit. */
286    border: 1px solid var(--border); /* Show the input boundary. */
287    border-radius: 6px; /* Match the code-panel corners. */
288    background: var(--bg); /* Follow the selected theme. */
289    color: var(--fg); /* Keep the search text readable. */
290    font: inherit; /* Use the same type size as the prose. */
291} /* Finish the search control styles. */
292.muted { font-size: 14px; opacity: .78; } /* De-emphasize counts and generation notes. */
293.hidden { display: none; } /* Hide entries excluded by the search. */
294table { border-collapse: collapse; width: 100%; } /* Keep requirement columns aligned. */
295th, td { padding: 8px; border-bottom: 1px solid var(--border); text-align: left; } /* Space table cells. */
296.source-code code { white-space: pre; } /* Preserve exact header indentation. */
297.line:target { background: var(--border); } /* Highlight a linked header line. */
298.line-number { /* Keep source line numbers separate from the code. */
299    display: inline-block; /* Reserve a consistent gutter. */
300    width: 5em; /* Leave room for large header line numbers. */
301    text-align: right; /* Align numbers by their final digit. */
302    padding-right: 1.5em; /* Separate numbers from the source text. */
303    opacity: .65; /* Keep the source itself more prominent. */
304    text-decoration: none; /* Avoid underlining every line number. */
305} /* Finish the source gutter styles. */
306</style> <!-- All page styling is embedded above. -->
307<link rel="stylesheet" href="site.css"> <!-- Match the navigation and appearance of the other documentation pages. -->
308</head> <!-- The remaining page contains the API itself. -->
309<body> <!-- Display the guide and its generated declaration cards. -->
310<a class="skip-link" href="#content">Skip to content</a> <!-- Let keyboard readers skip the repeated navigation. -->
311<header class="site-header"><div class="header-inner"><a class="brand" href="index.html">shdr<span class="release-version">__SHDR_DOC_VERSION__</span><span>CPU + GPU compute</span></a><nav class="site-nav" aria-label="Main navigation"><a href="index.html">Start</a><a href="examples.html">Examples</a><a href="results.html">Results</a><a href="corpus.html">Public shaders</a><a href="api.html" aria-current="page">API</a></nav></div></header> <!-- Connect the reference to the complete documentation site. -->
312<div class="page-shell"><main class="page-main" id="content"> <!-- Keep the page within the readable content column. -->
313<p class="eyebrow">Runtime reference</p> <!-- Distinguish declarations from the beginner walkthrough. -->
314<h1>shdr __SHDR_DOC_VERSION__ API reference</h1> <!-- Name the library and release being documented. -->
315<p>A C++11 header for CPU and GPU compute pipelines. The build helper turns native GLSL into CPU adapters and embeds GPU source. Applications create kernels, bind resources and run dispatches. <a href="source/shdr.h.html">Read or download the single-file runtime header</a>.</p> <!-- Connect the API to the runtime used by an application. -->
316<p>Start with the <a href="index.html">walkthrough</a>. Read about <a href="resources.html">resources and bindings</a>, <a href="portability.html">supported shader features</a>, <a href="async.html">asynchronous execution</a>, <a href="imaging.html">imaging</a>, <a href="evt3.html">event-camera processing</a>, <a href="neural.html">neural models</a>, <a href="signals.html">signal processing</a> and <a href="datasets.html">public data</a>. The <a href="examples.html">example catalog</a> connects each complete program to its kernels.</p> <!-- Link the beginner guides. -->
317<table> <!-- Compare the backend requirements. -->
318<thead><tr><th scope="col">Backend</th><th scope="col">Minimum requirement</th></tr></thead> <!-- Label each requirement column. -->
319<tbody> <!-- List the supported execution choices. -->
320<tr><td>CPU</td><td>C++11 compiler and host resources.</td></tr> <!-- State the CPU requirement. -->
321<tr><td>OpenGL ES</td><td>ES 3.1 with GLSL ES 3.10, or ES 3.2 with GLSL ES 3.20.</td></tr> <!-- State the GLES profiles. -->
322<tr><td>Desktop OpenGL</td><td>GL 4.3 with GLSL 4.30, or a newer supported profile.</td></tr> <!-- State the desktop minimum. -->
323</tbody> <!-- Finish the backend choices. -->
324</table> <!-- Finish the requirement comparison. -->
325<p>Enable <code>SHDR_ENABLE_EGL=1</code> and link EGL to create a headless GPU device. An existing application context can supply its own function resolver. GPU calls and final resource release use the context thread. The runtime handles transfers, dispatches, fence polling and bounded CPU work queues.</p> <!-- Explain context ownership and execution. -->
326<nav class="section-nav" aria-label="API sections">"""
327        + "\n".join(navigation)  # Insert the generated links to all nonempty API groups.
328        + """</nav>
329<label for="search">Find an API declaration</label><input id="search" type="search" placeholder="Try buffer, bind, glsl_source, pipeline, or imageLoad" autocomplete="off">
330<p class="muted" id="count" aria-live="polite">"""
331        + str(len(entries))  # Show the number of documented public entries.
332        + """ documented declarations.</p>
333"""
334        + "\n".join(sections)  # Insert the rendered declaration sections in display order.
335        + """
336<details id="source"><summary>Browse the complete documented header source</summary><pre class="source-code"><code class="syntax-highlight">"""
337        + "\n".join(  # Add the escaped header text to the source browser.
338            f'<span class="line" id="L{line}"><a class="line-number" href="#L{line}">{line}</a>{text}</span>'  # Keep balanced token spans inside their numbered source line.
339            for line, text in enumerate(highlight_lines(source, "cpp")[: len(source.splitlines())], 1)  # Tokenize the complete header so multiline comments retain their state.
340        )  # Finish this part of the API page.
341        + """</code></pre></details>
342<script> // Keep search and source navigation available without external scripts.
343const search = document.getElementById('search'); // Find the search input.
344const entries = Array.from(document.querySelectorAll('.entry')); // Keep all declaration cards.
345search.addEventListener('input', () => { // Refresh results after each text edit.
346    const terms = search.value.toLowerCase().trim().split(/\\s+/).filter(Boolean); // Ignore case and empty words.
347    let count = 0; // Count the entries that match every search word.
348    entries.forEach(entry => { // Inspect each declaration's searchable text.
349        const visible = terms.every(term => entry.dataset.search.includes(term)); // Require all words to match.
350        entry.classList.toggle('hidden', !visible); // Hide unmatched declaration cards.
351        if (visible) count++; // Count visible cards for the status text.
352    }); // Every declaration now has its current visibility.
353    document.querySelectorAll('section').forEach(section => { // Hide groups with no visible declarations.
354        const visible = section.querySelector('.entry:not(.hidden)'); // Find any visible card in this group.
355        section.classList.toggle('hidden', !visible); // Keep only groups with results.
356    }); // Group headings now match their visible cards.
357    document.getElementById('count').textContent = count + ' documented declarations match.'; // Show the result count.
358}); // The input listener handles each new query.
359function showSource() { // Open the header when a source-line link is selected.
360    if (/^#L[0-9]+$/.test(location.hash)) { // Recognize only generated line anchors.
361        document.getElementById('source').open = true; // Expand the source browser.
362        const line = document.getElementById(location.hash.slice(1)); // Locate the linked line.
363        if (line) line.scrollIntoView(); // Move the selected line into view.
364    } // Ordinary section links leave the source browser unchanged.
365} // Source navigation is also used for direct page links.
366document.addEventListener('click', event => { // Open the source before scrolling to its anchor.
367    if (event.target.closest('a[href^="#L"]')) { // Recognize a source-line link.
368        document.getElementById('source').open = true; // Reveal the line's enclosing section.
369    } // Other page links keep their usual behavior.
370}); // Clicking a line link can now scroll to visible content.
371window.addEventListener('hashchange', showSource); // Handle forward and back navigation.
372showSource(); // Honor a source-line anchor in the initial page URL.
373</script> <!-- Finish the embedded page behavior. -->
374</main></div> <!-- Finish the documentation column. -->
375<footer class="footer"><p>shdr __SHDR_DOC_VERSION__ · C++11 compute pipelines · <a href="portability.html">Portability and requirements</a> · <a href="source/index.html">Read the source</a></p></footer> <!-- Keep the next useful links available after the reference. -->
376</body> <!-- Finish the visible page. -->
377</html> <!-- The reference needs no network resources. -->
378"""
379    )  # Finish this part of the API page.
380    document = document.replace("__SHDR_DOC_VERSION__", html.escape(version))  # Apply the header's release label to page chrome only.
381    document = add_stylesheet(document, "highlight.css")  # Load the local stylesheet for precomputed syntax colors.
382    output.parent.mkdir(parents=True, exist_ok=True)  # Create the output directory before writing the page.
383    output.write_text(document, encoding="utf-8")  # Write the completed standalone API page.
384    return len(entries)  # Report the number of exported entries.
385
386
387def main() -> None:  # Read options and run this command-line tool.
388    """Parse optional paths and export the library's API docstrings."""
389    parser = argparse.ArgumentParser(description=__doc__)  # Describe the command and its supported options.
390    parser.add_argument("--header", type=Path, default=ROOT / "shdr.h")  # Allow an alternate header while defaulting to this checkout's shdr.h.
391    parser.add_argument("--output", type=Path, default=ROOT / "docs" / "api.html")  # Allow an alternate output while defaulting to docs/api.html.
392    args = parser.parse_args()  # Read the header and output paths selected by the user.
393    print(f"Exported {export(args.header, args.output)} documented declarations to {args.output}")  # Print the entry count and destination of the completed export.
394
395
396if __name__ == "__main__":  # Run only when this file is invoked as a command.
397    main()  # Read options and run the requested checks.