From fbc549a84507493e5aea069a5b0e673e5c9982b2 Mon Sep 17 00:00:00 2001 From: <> Date: Mon, 7 Aug 2023 15:48:37 +0000 Subject: [PATCH] Deployed c57ac36 with MkDocs version: 1.5.2 --- .nojekyll | 0 404.html | 440 ++ CNAME | 1 + assets/images/favicon.png | Bin 0 -> 1870 bytes assets/javascripts/bundle.220ee61c.min.js | 29 + assets/javascripts/bundle.220ee61c.min.js.map | 8 + assets/javascripts/lunr/min/lunr.ar.min.js | 1 + assets/javascripts/lunr/min/lunr.da.min.js | 18 + assets/javascripts/lunr/min/lunr.de.min.js | 18 + assets/javascripts/lunr/min/lunr.du.min.js | 18 + assets/javascripts/lunr/min/lunr.es.min.js | 18 + assets/javascripts/lunr/min/lunr.fi.min.js | 18 + assets/javascripts/lunr/min/lunr.fr.min.js | 18 + assets/javascripts/lunr/min/lunr.hi.min.js | 1 + assets/javascripts/lunr/min/lunr.hu.min.js | 18 + assets/javascripts/lunr/min/lunr.hy.min.js | 1 + assets/javascripts/lunr/min/lunr.it.min.js | 18 + assets/javascripts/lunr/min/lunr.ja.min.js | 1 + assets/javascripts/lunr/min/lunr.jp.min.js | 1 + assets/javascripts/lunr/min/lunr.kn.min.js | 1 + assets/javascripts/lunr/min/lunr.ko.min.js | 1 + assets/javascripts/lunr/min/lunr.multi.min.js | 1 + assets/javascripts/lunr/min/lunr.nl.min.js | 18 + assets/javascripts/lunr/min/lunr.no.min.js | 18 + assets/javascripts/lunr/min/lunr.pt.min.js | 18 + assets/javascripts/lunr/min/lunr.ro.min.js | 18 + assets/javascripts/lunr/min/lunr.ru.min.js | 18 + assets/javascripts/lunr/min/lunr.sa.min.js | 1 + .../lunr/min/lunr.stemmer.support.min.js | 1 + assets/javascripts/lunr/min/lunr.sv.min.js | 18 + assets/javascripts/lunr/min/lunr.ta.min.js | 1 + assets/javascripts/lunr/min/lunr.te.min.js | 1 + assets/javascripts/lunr/min/lunr.th.min.js | 1 + assets/javascripts/lunr/min/lunr.tr.min.js | 18 + assets/javascripts/lunr/min/lunr.vi.min.js | 1 + assets/javascripts/lunr/min/lunr.zh.min.js | 1 + assets/javascripts/lunr/tinyseg.js | 206 + assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.74e28a9f.min.js | 42 + .../workers/search.74e28a9f.min.js.map | 8 + assets/stylesheets/main.eebd395e.min.css | 1 + assets/stylesheets/main.eebd395e.min.css.map | 1 + assets/stylesheets/palette.ecc896b0.min.css | 1 + .../stylesheets/palette.ecc896b0.min.css.map | 1 + basic-rendering/index.html | 1304 ++++ custom-shaders/index.html | 1897 +++++ glsl/index.html | 1308 ++++ index.html | 558 ++ renderer/index.html | 1001 +++ search/search_index.json | 1 + sitemap.xml | 28 + sitemap.xml.gz | Bin 0 -> 244 bytes 52 files changed, 13828 insertions(+) create mode 100644 .nojekyll create mode 100644 404.html create mode 100644 CNAME create mode 100644 assets/images/favicon.png create mode 100644 assets/javascripts/bundle.220ee61c.min.js create mode 100644 assets/javascripts/bundle.220ee61c.min.js.map create mode 100644 assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hy.min.js create mode 100644 assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 assets/javascripts/lunr/min/lunr.kn.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ko.min.js create mode 100644 assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 assets/javascripts/lunr/min/lunr.sa.min.js create mode 100644 assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ta.min.js create mode 100644 assets/javascripts/lunr/min/lunr.te.min.js create mode 100644 assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 assets/javascripts/lunr/tinyseg.js create mode 100644 assets/javascripts/lunr/wordcut.js create mode 100644 assets/javascripts/workers/search.74e28a9f.min.js create mode 100644 assets/javascripts/workers/search.74e28a9f.min.js.map create mode 100644 assets/stylesheets/main.eebd395e.min.css create mode 100644 assets/stylesheets/main.eebd395e.min.css.map create mode 100644 assets/stylesheets/palette.ecc896b0.min.css create mode 100644 assets/stylesheets/palette.ecc896b0.min.css.map create mode 100644 basic-rendering/index.html create mode 100644 custom-shaders/index.html create mode 100644 glsl/index.html create mode 100644 index.html create mode 100644 renderer/index.html create mode 100644 search/search_index.json create mode 100644 sitemap.xml create mode 100644 sitemap.xml.gz diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..38fe2374 --- /dev/null +++ b/404.html @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + libplacebo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..3be539d1 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +libplacebo.org diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/assets/javascripts/bundle.220ee61c.min.js b/assets/javascripts/bundle.220ee61c.min.js new file mode 100644 index 00000000..116072a1 --- /dev/null +++ b/assets/javascripts/bundle.220ee61c.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Ci=Object.create;var gr=Object.defineProperty;var Ri=Object.getOwnPropertyDescriptor;var ki=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Hi=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,nn=Object.prototype.propertyIsEnumerable;var rn=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&rn(e,r,t[r]);if(Ht)for(var r of Ht(t))nn.call(t,r)&&rn(e,r,t[r]);return e};var on=(e,t)=>{var r={};for(var n in e)yr.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Ht)for(var n of Ht(e))t.indexOf(n)<0&&nn.call(e,n)&&(r[n]=e[n]);return r};var Pt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Pi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ki(t))!yr.call(e,o)&&o!==r&&gr(e,o,{get:()=>t[o],enumerable:!(n=Ri(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Ci(Hi(e)):{},Pi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var sn=Pt((xr,an)=>{(function(e,t){typeof xr=="object"&&typeof an!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(xr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function f(O){var Qe=O.type,De=O.tagName;return!!(De==="INPUT"&&s[Qe]&&!O.readOnly||De==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function c(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){O.hasAttribute("data-focus-visible-added")&&(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(a(r.activeElement)&&c(r.activeElement),n=!0)}function m(O){n=!1}function d(O){a(O.target)&&(n||f(O.target))&&c(O.target)}function h(O){a(O.target)&&(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function v(O){document.visibilityState==="hidden"&&(o&&(n=!0),Y())}function Y(){document.addEventListener("mousemove",N),document.addEventListener("mousedown",N),document.addEventListener("mouseup",N),document.addEventListener("pointermove",N),document.addEventListener("pointerdown",N),document.addEventListener("pointerup",N),document.addEventListener("touchmove",N),document.addEventListener("touchstart",N),document.addEventListener("touchend",N)}function B(){document.removeEventListener("mousemove",N),document.removeEventListener("mousedown",N),document.removeEventListener("mouseup",N),document.removeEventListener("pointermove",N),document.removeEventListener("pointerdown",N),document.removeEventListener("pointerup",N),document.removeEventListener("touchmove",N),document.removeEventListener("touchstart",N),document.removeEventListener("touchend",N)}function N(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,B())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),Y(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var cn=Pt(Er=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},s=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(B,N){d.append(N,B)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+c+" due to "+O)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,Y=!0,B=this;["append","delete","set"].forEach(function(O){var Qe=h[O];h[O]=function(){Qe.apply(h,arguments),v&&(Y=!1,B.search=h.toString(),Y=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var N=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==N&&(N=this.search,Y&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},s=i.prototype,a=function(f){Object.defineProperty(s,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){a(f)}),Object.defineProperty(s,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er)});var qr=Pt((Mt,Nr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Mt=="object"&&typeof Nr=="object"?Nr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return Ai}});var s=i(279),a=i.n(s),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(T){return!1}}var d=function(T){var E=p()(T);return m("cut"),E},h=d;function v(j){var T=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[T?"right":"left"]="-9999px";var H=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(H,"px"),E.setAttribute("readonly",""),E.value=j,E}var Y=function(T,E){var H=v(T);E.container.appendChild(H);var I=p()(H);return m("copy"),H.remove(),I},B=function(T){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},H="";return typeof T=="string"?H=Y(T,E):T instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(T==null?void 0:T.type)?H=Y(T.value,E):(H=p()(T),m("copy")),H},N=B;function O(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(E){return typeof E}:O=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},O(j)}var Qe=function(){var T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=T.action,H=E===void 0?"copy":E,I=T.container,q=T.target,Me=T.text;if(H!=="copy"&&H!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(H==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(H==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return N(Me,{container:I});if(q)return H==="cut"?h(q):N(q,{container:I})},De=Qe;function $e(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?$e=function(E){return typeof E}:$e=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},$e(j)}function Ei(j,T){if(!(j instanceof T))throw new TypeError("Cannot call a class as a function")}function tn(j,T){for(var E=0;E0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof I.action=="function"?I.action:this.defaultAction,this.target=typeof I.target=="function"?I.target:this.defaultTarget,this.text=typeof I.text=="function"?I.text:this.defaultText,this.container=$e(I.container)==="object"?I.container:document.body}},{key:"listenClick",value:function(I){var q=this;this.listener=c()(I,"click",function(Me){return q.onClick(Me)})}},{key:"onClick",value:function(I){var q=I.delegateTarget||I.currentTarget,Me=this.action(q)||"copy",kt=De({action:Me,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Me,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(I){return vr("action",I)}},{key:"defaultTarget",value:function(I){var q=vr("target",I);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(I){return vr("text",I)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(I){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return N(I,q)}},{key:"cut",value:function(I){return h(I)}},{key:"isSupported",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof I=="string"?[I]:I,Me=!!document.queryCommandSupported;return q.forEach(function(kt){Me=Me&&!!document.queryCommandSupported(kt)}),Me}}]),E}(a()),Ai=Li},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,f){for(;a&&a.nodeType!==o;){if(typeof a.matches=="function"&&a.matches(f))return a;a=a.parentNode}}n.exports=s},438:function(n,o,i){var s=i(828);function a(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof m=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=s(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(n,o,i){var s=i(879),a=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(h))throw new TypeError("Third argument must be a Function");if(s.node(m))return c(m,d,h);if(s.nodeList(m))return u(m,d,h);if(s.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return a(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),s=f.toString()}return s}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,s,a){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var f=this;function c(){f.off(i,c),s.apply(a,arguments)}return c._=s,this.on(i,c,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=a.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var rs=/["'&<>]/;Yo.exports=ns;function ns(e){var t=""+e,r=rs.exec(t);if(!r)return t;var n,o="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],s;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(a){s={error:a}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||a(m,d)})})}function a(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof et?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){a("next",m)}function u(m){a("throw",m)}function p(m,d){m(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function pn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Ee=="function"?Ee(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(s){return new Promise(function(a,f){s=e[i](s),o(a,f,s.done,s.value)})}}function o(i,s,a,f){Promise.resolve(f).then(function(c){i({value:c,done:a})},s)}}function C(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ie=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Ee(s),f=a.next();!f.done;f=a.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var u=this.initialTeardown;if(C(u))try{u()}catch(v){i=v instanceof It?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=Ee(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{ln(h)}catch(v){i=i!=null?i:[],v instanceof It?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ln(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Sr=Ie.EMPTY;function jt(e){return e instanceof Ie||e&&"closed"in e&&C(e.remove)&&C(e.add)&&C(e.unsubscribe)}function ln(e){C(e)?e():e.unsubscribe()}var Le={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,s=o.isStopped,a=o.observers;return i||s?Sr:(this.currentObservers=null,a.push(r),new Ie(function(){n.currentObservers=null,Ve(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,s=n.isStopped;o?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new xn(r,n)},t}(F);var xn=function(e){ie(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Sr},t}(x);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ie(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,s=n._infiniteTimeWindow,a=n._timestampProvider,f=n._windowTime;o||(i.push(r),!s&&i.push(a.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,s=o._buffer,a=s.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var s=r.actions;n!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Wt);var Sn=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Dt);var Oe=new Sn(wn);var M=new F(function(e){return e.complete()});function Vt(e){return e&&C(e.schedule)}function Cr(e){return e[e.length-1]}function Ye(e){return C(Cr(e))?e.pop():void 0}function Te(e){return Vt(Cr(e))?e.pop():void 0}function zt(e,t){return typeof Cr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Nt(e){return C(e==null?void 0:e.then)}function qt(e){return C(e[ft])}function Kt(e){return Symbol.asyncIterator&&C(e==null?void 0:e[Symbol.asyncIterator])}function Qt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Yt=zi();function Gt(e){return C(e==null?void 0:e[Yt])}function Bt(e){return un(this,arguments,function(){var r,n,o,i;return $t(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,et(r.read())];case 3:return n=s.sent(),o=n.value,i=n.done,i?[4,et(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,et(o)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Jt(e){return C(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(qt(e))return Ni(e);if(pt(e))return qi(e);if(Nt(e))return Ki(e);if(Kt(e))return On(e);if(Gt(e))return Qi(e);if(Jt(e))return Yi(e)}throw Qt(e)}function Ni(e){return new F(function(t){var r=e[ft]();if(C(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function qi(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?A(function(o,i){return e(o,i,n)}):de,ge(1),r?He(t):Dn(function(){return new Zt}))}}function Vn(){for(var e=[],t=0;t=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new x}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,f=a===void 0?!0:a;return function(c){var u,p,m,d=0,h=!1,v=!1,Y=function(){p==null||p.unsubscribe(),p=void 0},B=function(){Y(),u=m=void 0,h=v=!1},N=function(){var O=u;B(),O==null||O.unsubscribe()};return y(function(O,Qe){d++,!v&&!h&&Y();var De=m=m!=null?m:r();Qe.add(function(){d--,d===0&&!v&&!h&&(p=$r(N,f))}),De.subscribe(Qe),!u&&d>0&&(u=new rt({next:function($e){return De.next($e)},error:function($e){v=!0,Y(),p=$r(B,o,$e),De.error($e)},complete:function(){h=!0,Y(),p=$r(B,s),De.complete()}}),U(O).subscribe(u))})(c)}}function $r(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function z(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),V(e===_e()),J())}function Xe(e){return{x:e.offsetLeft,y:e.offsetTop}}function Kn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>Xe(e)),V(Xe(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>rr(e)),V(rr(e)))}var Yn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Wr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),va?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Wr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ba.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Gn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Jn=typeof WeakMap!="undefined"?new WeakMap:new Yn,Xn=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=ga.getInstance(),n=new La(t,r,this);Jn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Xn.prototype[e]=function(){var t;return(t=Jn.get(this))[e].apply(t,arguments)}});var Aa=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:Xn}(),Zn=Aa;var eo=new x,Ca=$(()=>k(new Zn(e=>{for(let t of e)eo.next(t)}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ye(e){return Ca.pipe(S(t=>t.observe(e)),g(t=>eo.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(()=>he(e)))),V(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var to=new x,Ra=$(()=>k(new IntersectionObserver(e=>{for(let t of e)to.next(t)},{threshold:0}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function sr(e){return Ra.pipe(S(t=>t.observe(e)),g(t=>to.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function ro(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=he(e),o=bt(e);return r>=o.height-n.height-t}),J())}var cr={drawer:z("[data-md-toggle=drawer]"),search:z("[data-md-toggle=search]")};function no(e){return cr[e].checked}function Ke(e,t){cr[e].checked!==t&&cr[e].click()}function Ue(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),V(t.checked))}function ka(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ha(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(V(!1))}function oo(){let e=b(window,"keydown").pipe(A(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:no("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),A(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!ka(n,r)}return!0}),pe());return Ha().pipe(g(t=>t?M:e))}function le(){return new URL(location.href)}function ot(e){location.href=e.href}function io(){return new x}function ao(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)ao(e,r)}function _(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)ao(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function so(){return location.hash.substring(1)}function Dr(e){let t=_("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Pa(e){return L(b(window,"hashchange"),e).pipe(l(so),V(so()),A(t=>t.length>0),X(1))}function co(e){return Pa(e).pipe(l(t=>ce(`[id="${t}"]`)),A(t=>typeof t!="undefined"))}function Vr(e){let t=matchMedia(e);return er(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function fo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(V(e.matches))}function zr(e,t){return e.pipe(g(r=>r?t():M))}function ur(e,t={credentials:"same-origin"}){return ue(fetch(`${e}`,t)).pipe(fe(()=>M),g(r=>r.status!==200?Ot(()=>new Error(r.statusText)):k(r)))}function We(e,t){return ur(e,t).pipe(g(r=>r.json()),X(1))}function uo(e,t){let r=new DOMParser;return ur(e,t).pipe(g(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),X(1))}function pr(e){let t=_("script",{src:e});return $(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(g(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),R(()=>document.head.removeChild(t)),ge(1))))}function po(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function lo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(po),V(po()))}function mo(){return{width:innerWidth,height:innerHeight}}function ho(){return b(window,"resize",{passive:!0}).pipe(l(mo),V(mo()))}function bo(){return G([lo(),ho()]).pipe(l(([e,t])=>({offset:e,size:t})),X(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(ee("size")),o=G([n,r]).pipe(l(()=>Xe(e)));return G([r,t,o]).pipe(l(([{height:i},{offset:s,size:a},{x:f,y:c}])=>({offset:{x:s.x-f,y:s.y-c+i},size:a})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(s=>{let a=document.createElement("script");a.src=i,a.onload=s,document.body.appendChild(a)})),Promise.resolve())}var r=class extends EventTarget{constructor(n){super(),this.url=n,this.m=i=>{i.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:i.data})),this.onmessage&&this.onmessage(i))},this.e=(i,s,a,f,c)=>{if(s===`${this.url}`){let u=new ErrorEvent("error",{message:i,filename:s,lineno:a,colno:f,error:c});this.dispatchEvent(u),this.onerror&&this.onerror(u)}};let o=document.createElement("iframe");o.hidden=!0,document.body.appendChild(this.iframe=o),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Basic windowing / output example

+

We will demonstrate the basics of the libplacebo GPU output API with a worked +example. The goal is to show a simple color on screen.

+

Creating a pl_log

+

Almost all major entry-points into libplacebo require providing a log +callback (or NULL to disable logging). This is abstracted into the pl_log +object type, which we can create with +pl_log_create:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
#include <libplacebo/log.h>
+
+pl_log pllog;
+
+int main()
+{
+    pllog = pl_log_create(PL_API_VER, pl_log_params(
+        .log_cb = pl_log_color,
+        .log_level = PL_LOG_INFO,
+    ));
+
+    // ...
+
+    pl_log_destroy(&pllog);
+    return 0;
+}
+
+
+

Compiling

+

You can compile this example with:

+
$ gcc example.c -o example `pkg-config --cflags --libs libplacebo`
+
+
+

The parameter PL_API_VER has no special significance and is merely included +for historical reasons. Aside from that, this snippet introduces a number of +core concepts of the libplacebo API:

+

Parameter structs

+

For extensibility, almost all libplacebo calls take a pointer to a const +struct pl_*_params, into which all extensible parameters go. For convenience, +libplacebo provides macros which create anonymous params structs on the stack +(and also fill in default parameters). Note that this only works for C99 and +above, users of C89 and C++ must initialize parameter structs manually.

+

Under the hood, pl_log_params(...) just translates to &((struct +pl_log_params) { /* default params */, ... }). This style of API allows +libplacebo to effectively simulate optional named parameters.

+
+

On default parameters

+

Wherever possible, parameters are designed in such a way that {0} gives +you a minimal parameter structure, with default behavior and no optional +features enabled. This is done for forwards compatibility - as new +features are introduced, old struct initializers will simply opt out of +them.

+
+

Destructors

+

All libplacebo objects must be destroyed manually using the corresponding +pl_*_destroy call, which takes a pointer to the variable the object is +stored in. The resulting variable is written to NULL. This helps prevent +use-after-free bugs.

+
+

NULL

+

As a general rule, all libplacebo destructors are safe to call on +variables containing NULL. So, users need not explicitly NULL-test +before calling destructors on variables.

+
+

Creating a window

+

While libplacebo can work in isolation, to render images offline, for the sake +of this guide we want to provide something graphical on-screen. As such, we +need to create some sort of window. Libplacebo provides no built-in mechanism +for this, it assumes the API user will already have a windowing system +in-place.

+

Complete examples (based on GLFW and SDL) can be found in the libplacebo +demos. But +for now, we will focus on getting a very simple window on-screen using GLFW:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
// ...
+
+#include <GLFW/glfw3.h>
+
+const char * const title = "libplacebo demo";
+int width = 800;
+int height = 600;
+
+GLFWwindow *window;
+
+int main()
+{
+    pllog = pl_log_create(PL_API_VER, pl_log_params(
+        .log_level = PL_LOG_INFO,
+    ));
+
+    if (!glfwInit())
+        return 1;
+
+    window = glfwCreateWindow(width, height, title, NULL, NULL);
+    if (!window)
+        return 1;
+
+    while (!glfwWindowShouldClose(window)) {
+        glfwWaitEvents();
+    }
+
+    glfwDestroyWindow(window);
+    glfwTerminate();
+    pl_log_destroy(&pllog);
+    return 0;
+}
+
+
+

Compiling

+

We now also need to include the glfw3 library to compile this example.

+
$ gcc example.c -o example `pkg-config --cflags --libs glfw3 libplacebo`
+
+
+

Creating the pl_gpu

+

All GPU operations are abstracted into an internal pl_gpu object, which +serves as the primary entry-point to any sort of GPU interaction. This object +cannot be created directly, but must be obtained from some graphical API: +currently there are Vulkan, OpenGL or D3D11. A pl_gpu can be accessed from +an API-specific object like pl_vulkan, pl_opengl and pl_d3d11.

+

In this guide, for simplicity, we will be using OpenGL, simply because that's +what GLFW initializes by default.

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
// ...
+
+pl_opengl opengl;
+
+static bool make_current(void *priv);
+static void release_current(void *priv);
+
+int main()
+{
+    // ...
+    window = glfwCreateWindow(width, height, title, NULL, NULL);
+    if (!window)
+        return 1;
+
+    opengl = pl_opengl_create(pllog, pl_opengl_params(
+        .get_proc_addr      = glfwGetProcAddress,
+        .allow_software     = true,         // allow software rasterers
+        .debug              = true,         // enable error reporting
+        .make_current       = make_current, // (1)
+        .release_current    = release_current,
+    ));
+    if (!opengl)
+        return 2;
+
+    while (!glfwWindowShouldClose(window)) {
+        glfwWaitEvents();
+    }
+
+    pl_opengl_destroy(&opengl);
+    glfwDestroyWindow(window);
+    glfwTerminate();
+    pl_log_destroy(&pllog);
+    return 0;
+}
+
+static bool make_current(void *priv)
+{
+    glfwMakeContextCurrent(window);
+    return true;
+}
+
+static void release_current(void *priv)
+{
+    glfwMakeContextCurrent(NULL);
+}
+
+
    +
  1. +

    Setting this allows the resulting pl_gpu to be thread-safe, which + enables asynchronous transfers to be used. The alternative is to simply + call glfwMakeContextCurrent once after creating the window.

    +

    This method of making the context current is generally preferred, +however, so we've demonstrated it here for completeness' sake.

    +
  2. +
+

Creating a swapchain

+

All access to window-based rendering commands are abstracted into an object +known as a "swapchain" (from Vulkan terminology), including the default +backbuffers on D3D11 and OpenGL. If we want to present something to screen, +we need to first create a pl_swapchain.

+

We can use this swapchain to perform the equivalent of gl*SwapBuffers:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
// ...
+pl_swapchain swchain;
+
+static void resize_cb(GLFWwindow *win, int new_w, int new_h)
+{
+    width  = new_w;
+    height = new_h;
+    pl_swapchain_resize(swchain, &width, &height);
+}
+
+int main()
+{
+    // ...
+    if (!opengl)
+        return 2;
+
+    swchain = pl_opengl_create_swapchain(opengl, pl_opengl_swapchain_params(
+        .swap_buffers   = (void (*)(void *)) glfwSwapBuffers,
+        .priv           = window,
+    ));
+    if (!swchain)
+        return 2;
+
+    // (2)
+    if (!pl_swapchain_resize(swchain, &width, &height))
+        return 2;
+    glfwSetFramebufferSizeCallback(window, resize_cb);
+
+    while (!glfwWindowShouldClose(window)) {
+        pl_swapchain_swap_buffers(swchain);
+        glfwPollEvents(); // (1)
+    }
+
+    pl_swapchain_destroy(&swchain);
+    pl_opengl_destroy(&opengl);
+    glfwDestroyWindow(window);
+    glfwTerminate();
+    pl_log_destroy(&pllog);
+    return 0;
+}
+
+
    +
  1. +

    We change this from glfwWaitEvents to glfwPollEvents because + we now want to re-run our main loop once per vsync, rather than only when + new events arrive. The pl_swapchain_swap_buffers call will ensure + that this does not execute too quickly.

    +
  2. +
  3. +

    The swapchain needs to be resized to fit the size of the window, which in + GLFW is handled by listening to a callback. In addition to setting this + callback, we also need to inform the swapchain of the initial window size.

    +

    Note that the pl_swapchain_resize function handles both resize requests +and size queries - hence, the actual swapchain size is returned back to +the passed variables.

    +
  4. +
+

Getting pixels on the screen

+

With a swapchain in hand, we're now equipped to start drawing pixels to the +screen:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
// ...
+
+static void render_frame(struct pl_swapchain_frame frame)
+{
+    pl_gpu gpu = opengl->gpu;
+
+    pl_tex_clear(gpu, frame.fbo, (float[4]){ 1.0, 0.5, 0.0, 1.0 });
+}
+
+int main()
+{
+    // ...
+
+    while (!glfwWindowShouldClose(window)) {
+        struct pl_swapchain_frame frame;
+        while (!pl_swapchain_start_frame(swchain, &frame))
+            glfwWaitEvents(); // (1)
+        render_frame(frame);
+        if (!pl_swapchain_submit_frame(swchain))
+            break; // (2)
+
+        pl_swapchain_swap_buffers(swchain);
+        glfwPollEvents();
+    }
+
+    // ...
+}
+
+
    +
  1. +

    If pl_swapchain_start_frame fails, it typically means the window is + hidden, minimized or blocked. This is not a fatal condition, and as such + we simply want to process window events until we can resume rendering.

    +
  2. +
  3. +

    If pl_swapchain_submit_frame fails, it typically means the window has + been lost, and further rendering commands are not expected to succeed. + As such, in this case, we simply terminate the example program.

    +
  4. +
+

Our main render loop has changed into a combination of +pl_swapchain_start_frame, rendering, and pl_swapchain_submit_frame. To +start with, we simply use the pl_tex_clear function to blit a constant +orange color to the framebuffer.

+

Interlude: Rendering commands

+

The previous code snippet represented our first foray into the pl_gpu API. +For more detail on this API, see the GPU API section. But as a +general rule of thumb, all pl_gpu-level operations are thread safe, +asynchronous (except when returning something to the CPU), and internally +refcounted (so you can destroy all objects as soon as you no longer need the +reference).

+

In the example loop, pl_swapchain_swap_buffers is the only operation that +actually flushes commands to the GPU. You can force an early flush with +pl_gpu_flush() or pl_gpu_finish(), but other than that, commands will +"queue" internally and complete asynchronously at some unknown point in time, +until forward progress is needed (e.g. pl_tex_download).

+

Conclusion

+

We have demonstrated how to create a window, how to initialize the libplacebo +API, create a GPU instance based on OpenGL, and how to write a basic rendering +loop that blits a single color to the framebuffer.

+

Here is a complete transcript of the example we built in this section:

+
+Basic rendering +
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
#include <GLFW/glfw3.h>
+
+#include <libplacebo/log.h>
+#include <libplacebo/opengl.h>
+#include <libplacebo/gpu.h>
+
+const char * const title = "libplacebo demo";
+int width = 800;
+int height = 600;
+
+GLFWwindow *window;
+
+pl_log pllog;
+pl_opengl opengl;
+pl_swapchain swchain;
+
+static bool make_current(void *priv);
+static void release_current(void *priv);
+
+static void resize_cb(GLFWwindow *win, int new_w, int new_h)
+{
+    width  = new_w;
+    height = new_h;
+    pl_swapchain_resize(swchain, &width, &height);
+}
+
+static void render_frame(struct pl_swapchain_frame frame)
+{
+    pl_gpu gpu = opengl->gpu;
+
+    pl_tex_clear(gpu, frame.fbo, (float[4]){ 1.0, 0.5, 0.0, 1.0 });
+}
+
+int main()
+{
+    pllog = pl_log_create(PL_API_VER, pl_log_params(
+        .log_cb = pl_log_color,
+        .log_level = PL_LOG_INFO,
+    ));
+
+    if (!glfwInit())
+        return 1;
+
+    window = glfwCreateWindow(width, height, title, NULL, NULL);
+    if (!window)
+        return 1;
+
+    opengl = pl_opengl_create(pllog, pl_opengl_params(
+        .get_proc_addr      = glfwGetProcAddress,
+        .allow_software     = true,         // allow software rasterers
+        .debug              = true,         // enable error reporting
+        .make_current       = make_current,
+        .release_current    = release_current,
+    ));
+
+    swchain = pl_opengl_create_swapchain(opengl, pl_opengl_swapchain_params(
+        .swap_buffers   = (void (*)(void *)) glfwSwapBuffers,
+        .priv           = window,
+    ));
+    if (!swchain)
+        return 2;
+
+    if (!pl_swapchain_resize(swchain, &width, &height))
+        return 2;
+    glfwSetFramebufferSizeCallback(window, resize_cb);
+
+    while (!glfwWindowShouldClose(window)) {
+        struct pl_swapchain_frame frame;
+        while (!pl_swapchain_start_frame(swchain, &frame))
+            glfwWaitEvents();
+        render_frame(frame);
+        if (!pl_swapchain_submit_frame(swchain))
+            break;
+
+        pl_swapchain_swap_buffers(swchain);
+        glfwPollEvents();
+    }
+
+    pl_swapchain_destroy(&swchain);
+    pl_opengl_destroy(&opengl);
+    glfwDestroyWindow(window);
+    glfwTerminate();
+    pl_log_destroy(&pllog);
+    return 0;
+}
+
+static bool make_current(void *priv)
+{
+    glfwMakeContextCurrent(window);
+    return true;
+}
+
+static void release_current(void *priv)
+{
+    glfwMakeContextCurrent(NULL);
+}
+
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/custom-shaders/index.html b/custom-shaders/index.html new file mode 100644 index 00000000..35e8b2bb --- /dev/null +++ b/custom-shaders/index.html @@ -0,0 +1,1897 @@ + + + + + + + + + + + + + + + + + + + + + + + + Custom Shaders (mpv .hook syntax) - libplacebo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + +

Custom Shaders (mpv .hook syntax)

+

libplacebo supports the same custom shader syntax used by +mpv, with some important +changes. This document will serve as a complete reference for this syntax.

+

Overview

+

In general, user shaders are divided into distinct blocks. Each block can +define a shader, a texture, a buffer, or a tunable parameter. Each block +starts with a collection of header directives, which are lines starting with +the syntax //!.

+

As an example, here is a simple shader that simply inverts the video signal:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
//!HOOK LUMA
+//!HOOK RGB
+//!BIND HOOKED
+
+vec4 hook()
+{
+    vec4 color = HOOKED_texOff(0);
+    color.rgb = vec3(1.0) - color.rgb;
+    return color;
+}
+
+

This shader defines one block - a shader block which hooks into the two +texture stages LUMA and RGB, binds the hooked texture, inverts the value +of the rgb channels, and then returns the modified color.

+

Expressions

+

In a few contexts, shader directives accept arithmetic expressions, denoted by +<expr> in the listing below. For historical reasons, all expressions are +given in reverse polish notation +(RPN), and the only +value type is a floating point number. The following value types and +arithmetic operations are available:

+
    +
  • 1.234: Literal float constant, evaluates to itself.
  • +
  • NAME.w, NAME.width: Evaluates to the width of a texture with name NAME.
  • +
  • NAME.h, NAME.height: Evaluates to the height of a texture with name NAME.
  • +
  • PAR: Evaluates to the value of a tunable shader parameter with name PAR.
  • +
  • +: Evaluates to X+Y.
  • +
  • -: Evaluates to X-Y.
  • +
  • *: Evaluates to X*Y.
  • +
  • /: Evaluates to X/Y.
  • +
  • %: Evaluates to fmod(X, Y).
  • +
  • >: Evaluates to (X > Y) ? 1.0 : 0.0.
  • +
  • <: Evaluates to (X < Y) ? 1.0 : 0.0.
  • +
  • =: Evaluates to fuzzy_eq(X, Y) ? 1.0 : 0.0, with some tolerance to + allow for floating point inaccuracy. (Around 1 ppm)
  • +
  • !: Evaluates to X ? 0.0 : 1.0.
  • +
+

Note that + and * can be used as suitable replacements for the otherwise +absent boolean logic expressions (|| and &&).

+

Shaders

+

Shaders are the default block type, and have no special syntax to indicate +their presence. Shader stages contain raw GLSL code that will be +(conditionally) executed. This GLSL snippet must define a single function +vec4 hook(), or void hook() for compute shaders.

+

During the execution of any shader, the following global variables are made +available:

+
    +
  • int frame: A raw counter tracking the number of executions of this shader + stage.
  • +
  • float random: A pseudo-random float uniformly distributed in the range + [0,1).
  • +
  • vec2 input_size: The nominal size (in pixels) of the original input image.
  • +
  • vec2 target_size: The nominal size (in pixels) of the output rectangle.
  • +
  • vec2 tex_offset: The nominal offset (in pixels), of the original input crop.
  • +
  • vec4 linearize(vec4 color): Linearize the input color according to the + image's tagged gamma function.
  • +
  • vec4 delinearize(vec4 color): Opposite counterpart to linearize.
  • +
+

Shader stages accept the following directives:

+

HOOK <texture>

+

A HOOK directive determines when a shader stage is run. During internal +processing, libplacebo goes over a number of pre-defined hook points at set +points in the processing pipeline. It is only possible to intercept the image, +and run custom shaders, at these fixed hook points.

+

Here is a current list of hook points:

+
    +
  • RGB: Input plane containing RGB values
  • +
  • LUMA: Input plane containing a Y value
  • +
  • CHROMA: Input plane containing chroma values (one or both)
  • +
  • ALPHA: Input plane containing a single alpha value
  • +
  • XYZ: Input plane containing XYZ values
  • +
  • CHROMA_SCALED: Chroma plane, after merging and upscaling to luma size
  • +
  • ALPHA_SCALED: Alpha plane, after upscaling to luma size
  • +
  • NATIVE: Merged input planes, before any sort of color conversion (as-is)
  • +
  • MAIN: After conversion to RGB, before linearization/scaling
  • +
  • LINEAR: After conversion to linear light (for scaling purposes)
  • +
  • SIGMOID: After conversion to sigmoidized light (for scaling purposes)
  • +
  • PREKERNEL: Immediately before the execution of the main scaler kernel
  • +
  • POSTKERNEL: Immediately after the execution of the main scaler kernel
  • +
  • SCALED: After scaling, in either linear or non-linear light RGB
  • +
  • PREOUTPUT: After color conversion to target colorspace, before alpha blending
  • +
  • OUTPUT: After alpha blending, before dithering and final output pass
  • +
+
+

MAINPRESUB

+

In mpv, MAIN and MAINPRESUB are separate shader stages, because the +mpv option --blend-subtitles=video allows rendering overlays directly +onto the pre-scaled video stage. libplacebo does not support this feature, +and as such, the MAINPRESUB shader stage does not exist. It is still +valid to refer to this name in shaders, but it is handled identically to +MAIN.

+
+

It's possible for a hook point to never fire. For example, SIGMOID will not +fire when downscaling, as sigmoidization only happens when upscaling. +Similarly, LUMA/CHROMA will not fire on an RGB video and vice versa.

+

A single shader stage may hook multiple hook points simultaneously, for +example, to cover both LUMA and RGB cases with the same logic. (See the +example shader in the introduction)

+

BIND <texture>

+

The BIND directive makes a texture available for use in the shader. This can +be any of the previously named hook points, a custom texture define by a +TEXTURE block, a custom texture saved by a SAVE directive, or the special +value HOOKED which allows binding whatever texture hook dispatched this +shader stage.

+

A bound texture will define the following GLSL functions (as macros):

+
    +
  • sampler2D NAME_raw: A reference to the raw texture sampler itself.
  • +
  • vec2 NAME_pos: The texel coordinates of the current pixel.
  • +
  • vec2 NAME_map(ivec2 id): A function that maps from gl_GlobalInvocationID + to texel coordinates. (Compute shaders)
  • +
  • vec2 NAME_size: The size (in pixels) of the texture.
  • +
  • vec2 NAME_pt: Convenience macro for 1.0 / NAME_size. The size of a + single pixel (in texel coordinates).
  • +
  • vec2 NAME_off: The sample offset of the texture. Basically, the pixel + coordinates of the top-left corner of the sampled area.
  • +
  • float NAME_mul: The coefficient that must be multiplied into sampled + values in order to rescale them to [0,1].
  • +
  • vec4 NAME_tex(vec2 pos): A wrapper around NAME_mul * textureLod(NAME_raw, + pos, 0.0).
  • +
  • vec4 NAME_texOff(vec2 offset): A wrapper around NAME_tex(NAME_pos + NAME_pt * offset). + This can be used to easily access adjacent pixels, e.g. NAME_texOff(-1,2) + samples a pixel one to the left and two to the bottom of the current + location.
  • +
  • vec4 NAME_gather(vec2 pos, int c): A wrapper around + NAME_mul * textureGather(pos, c), with appropriate scaling. (Only when + supported1)
  • +
+
+

Rotation matrix

+

For compatibility with mpv, we also define a mat2 NAME_rot which is +simply equal to a 2x2 identity matrix. libplacebo never rotates input +planes - all rotation happens during the final output to the display.

+
+

This same directive can also be used to bind buffer blocks (i.e. +uniform/storage buffers), as defined by the BUFFER directive.

+

SAVE <texture>

+

By default, after execution of a shader stage, the resulting output is +captured back into the same hooked texture that triggered the shader. This +behavior can be overridden using the explicit SAVE directive. For example, +a shader might need access to a low-res version of the luma input texture in +order to process chroma:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
//!HOOK CHROMA
+//!BIND CHROMA
+//!BIND LUMA
+//!SAVE LUMA_LOWRES
+//!WIDTH CHROMA.w
+//!HEIGHT CHROMA.h
+
+vec4 hook()
+{
+    return LUMA_texOff(0);
+}
+
+

This shader binds both luma and chroma and resizes the luma plane down to the +size of the chroma plane, saving the result as a new texture LUMA_LOWRES. In +general, you can pick any name you want, here.

+

DESC <description>

+

This purely informative directive simply gives the shader stage a name. This +is the name that will be reported to the shader stage and execution time +metrics.

+

OFFSET <xo yo | ALIGN>

+

This directive indicates a pixel shift (offset) introduced by this pass. These +pixel offsets will be accumulated and corrected automatically as part of plane +alignment / main scaling.

+

A special value of ALIGN will attempt to counteract any existing offset of +the hooked texture by aligning it with reference plane (i.e. luma). This can +be used to e.g. introduce custom chroma scaling in a way that doesn't break +chroma subtexel offsets.

+

An example:

+
1
+2
+3
+4
+5
+6
+7
+8
+9
//!HOOK LUMA
+//!BIND HOOKED
+//!OFFSET 100.5 100.5
+
+vec4 hook()
+{
+    // Constant offset by N pixels towards the bottom right
+    return HOOKED_texOff(-vec2(100.5));
+}
+
+

This (slightly silly) shader simply shifts the entire sampled region to the +bottom right by 100.5 pixels, and propagates this shift to the main scaler +using the OFFSET directive. As such, the end result of this is that there is +no visible shift of the overall image, but some detail (~100 pixels) near the +bottom-right border is lost due to falling outside the bounds of the texture.

+

WIDTH <expr>, HEIGHT <expr>

+

These directives can be used to override the dimensions of the resulting +texture. Note that not all textures can be resized this way. Currently, only +RGB, LUMA, CHROMA, XYZ, NATIVE and MAIN are resizable. Trying to +save a texture with an incompatible size to any other shader stage will result +in an error.

+

WHEN <expr>

+

This directive takes an expression that can be used to make shader stages +conditionally executed. If this evaluates to 0, the shader stage will be +skipped.

+

Example:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
//!PARAM strength
+//!TYPE float
+//!MINIMUM 0
+1.0
+
+//!HOOK MAIN
+//!BIND HOOKED
+//!WHEN intensity 0 >
+//!DESC do something based on 'intensity'
+...
+
+

This example defines a shader stage that only conditionally executes itself +if the value of the intensity shader parameter is non-zero.

+

COMPONENTS <num>

+

This directive overrides the number of components present in a texture. +For example, if you want to extract a one-dimensional feature map from the +otherwise 3 or 4 dimensional MAIN texture, you can use this directive to +save on memory bandwidth and consumption by having libplacebo only allocate a +one-component texture to store the feature map in:

+
1
+2
+3
+4
//!HOOK MAIN
+//!BIND HOOKED
+//!SAVE featuremap
+//!COMPONENTS 1
+
+

COMPUTE <bw> <bh> [<tw> <th>]

+

This directive specifies that the shader should be treated as a compute +shader, with the block size bw and bh. The compute shader will be +dispatched with however many blocks are necessary to completely tile over the +output. Within each block, there will be tw*th threads, forming a single +work group. In other words: tw and th specify the work group size, which +can be different from the block size. So for example, a compute shader with +bw = bh = 32 and tw = th = 8 running on a 500x500 texture would dispatch +16x16 blocks (rounded up), each with 8x8 threads.

+

Instead of defining a vec4 hook(), compute shaders must define a void +hook() which results directly to the output texture, a writeonly image2D +out_image made available to the shader stage.

+

For example, here is a shader executing a single-pass 41x41 convolution +(average blur) on the luma plane, using a compute shader to share sampling +work between adjacent threads in a work group:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
//!HOOK LUMA
+//!BIND HOOKED
+//!COMPUTE 32 32
+//!DESC avg convolution
+
+// Kernel size, 41x41 as an example
+const ivec2 ksize = ivec2(41, 41);
+const ivec2 offset = ksize / 2;
+
+// We need to load extra source texels to account for padding due to kernel
+// overhang
+const ivec2 isize = ivec2(gl_WorkGroupSize) + ksize - 1;
+
+shared float inp[isize.y][isize.x];
+
+void hook()
+{
+    // load texels into shmem
+    ivec2 base = ivec2(gl_WorkGroupID) * ivec2(gl_WorkGroupSize);
+    for (uint y = gl_LocalInvocationID.y; y < isize.y; y += gl_WorkGroupSize.y) {
+        for (uint x = gl_LocalInvocationID.x; x < isize.x; x += gl_WorkGroupSize.x)
+            inp[y][x] = texelFetch(HOOKED_raw, base + ivec2(x,y) - offset, 0).x;
+    }
+
+    // synchronize threads
+    barrier();
+
+    // do convolution
+    float sum;
+    for (uint y = 0; y < ksize.y; y++) {
+        for (uint x = 0; x < ksize.x; x++)
+            sum += inp[gl_LocalInvocationID.y+y][gl_LocalInvocationID.x+x];
+    }
+
+    vec4 color = vec4(HOOKED_mul * sum / (ksize.x * ksize.y), 0, 0, 1);
+    imageStore(out_image, ivec2(gl_GlobalInvocationID), color);
+}
+
+

Textures

+

Custom textures can be defined and made available to shader stages using +TEXTURE blocks. These can be used to provide e.g. LUTs or pre-trained +weights.

+

The data for a texture is provided as a raw hexadecimal string encoding the +in-memory representation of a texture, according to its given texture format, +for example:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
//!TEXTURE COLORS
+//!SIZE 3 3
+//!FORMAT rgba32f
+//!FILTER NEAREST
+//!BORDER REPEAT
+0000803f000000000000000000000000000000000000803f00000000000000000000000
+0000000000000803f00000000000000000000803f0000803f000000000000803f000000
+000000803f000000000000803f0000803f00000000000000009a99993e9a99993e9a999
+93e000000009a99193F9A99193f9a99193f000000000000803f0000803f0000803f0000
+0000
+
+

Texture blocks accept the following directives:

+

TEXTURE <name>

+

This must be the first directive in a texture block, and marks it as such. The +name given is the name that the texture will be referred to (via BIND +directives).

+

SIZE <width> [<height> [<depth>]]

+

This directive gives the size of the texture, as integers. For example, +//!SIZE 512 512 marks a 512x512 texture block. Textures can be 1D, 2D or 3D +depending on the number of coordinates specified.

+

FORMAT <fmt>

+

This directive specifies the texture format. A complete list of known textures +is exposed as part of the pl_gpu struct metadata, but they follow the format +convention rgba8, rg16hf, rgba32f, r64i and so on.

+

FILTER <LINEAR | NEAREST>

+

This directive specifies the texture magnification/minification filter.

+

BORDER <CLAMP | REPEAT | MIRROR>

+

This directive specifies the border clamping method of the texture.

+

STORAGE

+

If present, this directive marks the texture as a storage image. It will still +be initialized with the initial values, but rather than being bound as a +read-only and immutable sampler2D, it is bound as a readwrite coherent +image2D. Such texture scan be used to, for example, store persistent state +across invocations of the shader.

+

Buffers

+

Custom uniform / storage shader buffer blocks can be defined using BUFFER +directives.

+

The (initial) data for a buffer is provided as a raw hexadecimal string +encoding the in-memory representation of a buffer in the corresponding GLSL +packing layout (std140 or std430 for uniform and storage blocks, +respectively):

+
1
+2
+3
+4
+5
+6
+7
+8
+9
//!BUFFER buf_uniform
+//!VAR float foo
+//!VAR float bar
+0000000000000000
+
+//!BUFFER buf_storage
+//!VAR vec2 bat
+//!VAR int big[32];
+//!STORAGE
+
+

Buffer blocks accept the following directives:

+

BUFFER <name>

+

This must be the first directive in a buffer block, and marks it as such. The +name given is mostly cosmetic, as individual variables can be accessed +directly using the names given in the corresponding VAR directives.

+

STORAGE

+

If present, this directive marks the buffer as a (readwrite coherent) shader +storage block, instead of a readonly uniform buffer block. Such storage blocks +can be used to track and evolve state across invocations of this shader.

+

Storage blocks may also be initialized with default data, but this is +optional. They can also be initialized as part of the first shader execution +(e.g. by testing for frame == 0).

+

VAR <type> <name>

+

This directive appends a new variable to the shader block, with GLSL type +<type> and shader name <name>. For example, VAR float foo introduces a +float foo; member into the buffer block, and VAR mat4 transform introduces +a mat4 transform; member.

+

It is also possible to introduce array variables, using [N] as part of the +variable name.

+

Tunable parameters

+

Finally, the PARAM directive allows introducing tunable shader parameters, +which are exposed programmatically as part of the C API (pl_hook).2

+

The default value of a parameter is given as the block body, for example:

+
1
+2
+3
+4
+5
+6
//!PARAM contrast
+//!DESC Gain to apply to image brightness
+//!TYPE float
+//!MINIMUM 0.0
+//!MAXIMUM 100.0
+1.0
+
+

Parameters accept the following directives:

+

PARAM <name>

+

This must be the first directive in a parameter block, and marks it as such. +The name given is the name that will be used to refer to this parameter in +GLSL code.

+

DESC <description>

+

This directive can be used to provide a friendlier description of the shader +parameter, exposed as part of the C API to end users.

+

MINIMUM <value>, MAXIMUM <value>

+

Provides the minimum/maximum value bound of this parameter. If absent, no +minimum/maximum is enforced.

+

TYPE [ENUM] <DEFINE | [DYNAMIC | CONSTANT] <type>>

+

This gives the type of the parameter, which determines what type of values it +can hold and how it will be made available to the shader. <type> must be +a scalar GLSL numeric type, such as int, float or uint.

+

If a type is ENUM, it is treated as an enumeration type. To use this, type +must either be int or DEFINE. Instead of providing a single default value, +the param body should be a list of all possible enumeration values (as separate +lines). These names will be made available inside the shader body (as a +#define), as well as inside RPN expressions (e.g. WHEN). The qualifiers +MINIMUM and MAXIMUM are ignored for ENUM parameters, with the value +range instead being set implicitly from the list of options.

+

The optional qualifiers DYNAMIC or CONSTANT mark the parameter as +dynamically changing and compile-time constant, respectively. A DYNAMIC +variable is assumed to change frequently, and will be grouped with other +frequently-changing input parameters. A CONSTANT parameter will be +introduced as a compile-time constant into the shader header, which means thy +can be used in e.g. constant expressions such as array sizes.3

+

Finally, the special type TYPE DEFINE marks a variable as a preprocessor +define, which can be used inside #if preprocessor expressions. For example:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
//!PARAM taps
+//!DESC Smoothing taps
+//!TYPE DEFINE
+//!MINIMUM 0
+//!MAXIMUM 5
+2
+
+//!HOOK LUMA
+//!BIND HOOKED
+const uint row_size = 2 * taps + 1;
+const float weights[row_size] = {
+#if taps == 0
+    1.0,
+#endif
+
+#if taps == 1
+    0.10650697891920,
+    0.78698604216159,
+    0.10650697891920,
+#endif
+
+#if taps == 2
+    0.05448868454964,
+    0.24420134200323,
+    0.40261994689424,
+    0.24420134200323,
+    0.05448868454964,
+#endif
+
+    // ...
+};
+
+

An example of an enum parameter:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
//!PARAM csp
+//!DESC Colorspace
+//!TYPE ENUM int
+BT709
+BT2020
+DCIP3
+
+//!HOOK MAIN
+//!BIND HOOKED
+const mat3 matrices[3] = {
+    mat3(...), // BT709
+    mat3(...), // BT2020
+    mat3(...), // DCIP3
+};
+
+#define MAT matrices[csp]
+// ...
+
+

Full example

+

A collection of full examples can be found in the mpv user shaders +wiki, but +here is an example of a parametrized Gaussian smoothed film grain compute +shader:

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
//!PARAM intensity
+//!DESC Film grain intensity
+//!TYPE float
+//!MINIMUM 0
+0.1
+
+//!PARAM taps
+//!DESC Film grain smoothing taps
+//!TYPE DEFINE
+//!MINIMUM 0
+//!MAXIMUM 5
+2
+
+//!HOOK LUMA
+//!BIND HOOKED
+//!DESC Apply gaussian smoothed film grain
+//!WHEN intensity 0 >
+//!COMPUTE 32 32
+
+const uint row_size = 2 * taps + 1;
+const float weights[row_size] = {
+#if taps == 0
+    1.0,
+#endif
+
+#if taps == 1
+    0.10650697891920,
+    0.78698604216159,
+    0.10650697891920,
+#endif
+
+#if taps == 2
+    0.05448868454964,
+    0.24420134200323,
+    0.40261994689424,
+    0.24420134200323,
+    0.05448868454964,
+#endif
+
+#if taps == 3
+    0.03663284536919,
+    0.11128075847888,
+    0.21674532140370,
+    0.27068214949642,
+    0.21674532140370,
+    0.11128075847888,
+    0.03663284536919,
+#endif
+
+#if taps == 4
+    0.02763055063889,
+    0.06628224528636,
+    0.12383153680577,
+    0.18017382291138,
+    0.20416368871516,
+    0.18017382291138,
+    0.12383153680577,
+    0.06628224528636,
+    0.02763055063889,
+#endif
+
+#if taps == 5
+    0.02219054849244,
+    0.04558899978527,
+    0.07981140824009,
+    0.11906462996609,
+    0.15136080967773,
+    0.16396720767670,
+    0.15136080967773,
+    0.11906462996609,
+    0.07981140824009,
+    0.04558899978527,
+    0.02219054849244,
+#endif
+};
+
+const uvec2 isize = uvec2(gl_WorkGroupSize) + uvec2(2 * taps);
+shared float grain[isize.y][isize.x];
+
+// PRNG
+float permute(float x)
+{
+    x = (34.0 * x + 1.0) * x;
+    return fract(x * 1.0/289.0) * 289.0;
+}
+
+float seed(uvec2 pos)
+{
+    const float phi = 1.61803398874989;
+    vec3 m = vec3(fract(phi * vec2(pos)), random) + vec3(1.0);
+    return permute(permute(m.x) + m.y) + m.z;
+}
+
+float rand(inout float state)
+{
+    state = permute(state);
+    return fract(state * 1.0/41.0);
+}
+
+// Turns uniform white noise into gaussian white noise by passing it
+// through an approximation of the gaussian quantile function
+float rand_gaussian(inout float state) {
+    const float a0 = 0.151015505647689;
+    const float a1 = -0.5303572634357367;
+    const float a2 = 1.365020122861334;
+    const float b0 = 0.132089632343748;
+    const float b1 = -0.7607324991323768;
+
+    float p = 0.95 * rand(state) + 0.025;
+    float q = p - 0.5;
+    float r = q * q;
+
+    float g = q * (a2 + (a1 * r + a0) / (r*r + b1*r + b0));
+    g *= 0.255121822830526; // normalize to [-1,1)
+    return g;
+}
+
+void hook()
+{
+    // generate grain in `grain`
+    uint num_threads = gl_WorkGroupSize.x * gl_WorkGroupSize.y;
+    for (uint i = gl_LocalInvocationIndex; i < isize.y * isize.x; i += num_threads) {
+        uvec2 pos = uvec2(i % isize.y, i / isize.y);
+        float state = seed(gl_WorkGroupID.xy * gl_WorkGroupSize.xy + pos);
+        grain[pos.y][pos.x] = rand_gaussian(state);
+    }
+
+    // make writes visible
+    barrier();
+
+    // convolve horizontally
+    for (uint y = gl_LocalInvocationID.y; y < isize.y; y += gl_WorkGroupSize.y) {
+        float hsum = 0;
+        for (uint x = 0; x < row_size; x++) {
+            float g = grain[y][gl_LocalInvocationID.x + x];
+            hsum += weights[x] * g;
+        }
+
+        // update grain LUT
+        grain[y][gl_LocalInvocationID.x + taps] = hsum;
+    }
+
+    barrier();
+
+    // convolve vertically
+    float vsum = 0.0;
+    for (uint y = 0; y < row_size; y++) {
+        float g = grain[gl_LocalInvocationID.y + y][gl_LocalInvocationID.x + taps];
+        vsum += weights[y] * g;
+    }
+
+    vec4 color = HOOKED_tex(HOOKED_pos);
+    color.rgb += vec3(intensity * vsum);
+    imageStore(out_image, ivec2(gl_GlobalInvocationID), color);
+}
+
+
+
+
    +
  1. +

    Because these are macros, their presence can be tested for using + #ifdef inside the GLSL preprocessor. 

    +
  2. +
  3. +

    In mpv using --vo=gpu-next, these can be set using the + --glsl-shader-opts option

    +
  4. +
  5. +

    On supported platforms, these are implemented using specialization + constants, which can be updated at run-time without requiring a full shader + recompilation. 

    +
  6. +
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/glsl/index.html b/glsl/index.html new file mode 100644 index 00000000..c8fb7e86 --- /dev/null +++ b/glsl/index.html @@ -0,0 +1,1308 @@ + + + + + + + + + + + + + + + + + + + + + + GLSL shader system - libplacebo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

GLSL shader system

+

Overall design

+

Shaders in libplacebo are all written in GLSL, and built up incrementally, on +demand. Generally, all shaders for each frame are generated per frame. So +functions like pl_shader_color_map etc. are run anew for every frame. This +makes the renderer very stateless and allows us to directly embed relevant +constants, uniforms etc. as part of the same code that generates the actual +GLSL shader.

+

To avoid this from becoming wasteful, libplacebo uses an internal string +building abstraction +(pl_str_builder). +Rather than building up a string directly, a pl_str_builder is like a list of +string building functions/callbacks to execute in order to generate the actual +shader. Combined with an efficient pl_str_builder_hash, this allows us to +avoid the bulk of the string templating work for already-cached shaders.

+

Legacy API

+

For the vast majority of libplacebo's history, the main entry-point into the +shader building mechanism was the GLSL() macro (and +variants), which works like a +printf-append:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
void pl_shader_extract_features(pl_shader sh, struct pl_color_space csp)
+{
+    if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
+        return;
+
+    sh_describe(sh, "feature extraction");
+    pl_shader_linearize(sh, &csp);
+    GLSL("// pl_shader_extract_features             \n"
+         "{                                         \n"
+         "vec3 lms = %f * "$" * color.rgb;          \n"
+         "lms = pow(max(lms, 0.0), vec3(%f));       \n"
+         "lms = (vec3(%f) + %f * lms)               \n"
+         "        / (vec3(1.0) + %f * lms);         \n"
+         "lms = pow(lms, vec3(%f));                 \n"
+         "float I = dot(vec3(%f, %f, %f), lms);     \n"
+         "color = vec4(I, 0.0, 0.0, 1.0);           \n"
+         "}                                         \n",
+         PL_COLOR_SDR_WHITE / 10000,
+         SH_MAT3(pl_ipt_rgb2lms(pl_raw_primaries_get(csp.primaries))),
+         PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2,
+         pl_ipt_lms2ipt.m[0][0], pl_ipt_lms2ipt.m[0][1], pl_ipt_lms2ipt.m[0][2]);
+}
+
+

The special macro $ is a stand-in for an identifier (ident_t), which is +the internal type used to pass references to loaded uniforms, descriptors and +so on:

+
typedef unsigned short ident_t;
+#define $           "_%hx"
+#define NULL_IDENT  0u
+
+// ...
+
+ident_t sh_var_mat3(pl_shader sh, const char *name, pl_matrix3x3 val);
+#define SH_MAT3(val) sh_var_mat3(sh, "mat", val)
+
+

In general, constants in libplacebo are divided into three categories:

+

Literal shader constants

+

These are values that are expected to change very infrequently (or never), or +for which we want to generate a different shader variant per value. Such values +should be directly formatted as numbers into the shader text: %d, %f and so +on. This is commonly used for array sizes, constants that depend only on +hardware limits, constants that never change (but which have a friendly name, +like PQ_C2 above), and so on.

+

As an example, the debanding iterations weights are hard-coded like this, +because the debanding shader is expected to change as a result of a different +number of iterations anyway:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
// For each iteration, compute the average at a given distance and
+// pick it instead of the color if the difference is below the threshold.
+for (int i = 1; i <= params->iterations; i++) {
+    GLSL(// Compute a random angle and distance
+         "d = "$".xy * vec2(%d.0 * "$", %f);    \n" // (1)
+         "d = d.x * vec2(cos(d.y), sin(d.y));   \n"
+         // Sample at quarter-turn intervals around the source pixel
+         "avg = T(0.0);                         \n"
+         "avg += GET(+d.x, +d.y);               \n"
+         "avg += GET(-d.x, +d.y);               \n"
+         "avg += GET(-d.x, -d.y);               \n"
+         "avg += GET(+d.x, -d.y);               \n"
+         "avg *= 0.25;                          \n"
+         // Compare the (normalized) average against the pixel
+         "diff = abs(res - avg);                \n"
+         "bound = T("$" / %d.0);                \n",
+         prng, i, radius, M_PI * 2,
+         threshold, i);
+
+    if (num_comps > 1) {
+        GLSL("res = mix(avg, res, greaterThan(diff, bound)); \n");
+    } else {
+        GLSL("res = mix(avg, res, diff > bound); \n");
+    }
+}
+
+
    +
  1. The %d.0 here corresponds to the iteration index i, while the %f + corresponds to the fixed constant M_PI * 2.
  2. +
+

Specializable shader constants

+

These are used for tunable parameters that are expected to change infrequently +during normal playback. These constitute by far the biggest category, and most +parameters coming from the various _params structs should be loaded like +this.

+

They are loaded using the sh_const_*() functions, which generate a +specialization constant on supported platforms, falling back to a literal +shader #define otherwise. For anoymous parameters, you can use the +short-hands SH_FLOAT, SH_INT etc.:

+
ident_t sh_const_int(pl_shader sh, const char *name, int val);
+ident_t sh_const_uint(pl_shader sh, const char *name, unsigned int val);
+ident_t sh_const_float(pl_shader sh, const char *name, float val);
+#define SH_INT(val)     sh_const_int(sh, "const", val)
+#define SH_UINT(val)    sh_const_uint(sh, "const", val)
+#define SH_FLOAT(val)   sh_const_float(sh, "const", val)
+
+

Here is an example of them in action:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
void pl_shader_sigmoidize(pl_shader sh, const struct pl_sigmoid_params *params)
+{
+    if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
+        return;
+
+    params = PL_DEF(params, &pl_sigmoid_default_params);
+    float center = PL_DEF(params->center, 0.75);
+    float slope  = PL_DEF(params->slope, 6.5);
+
+    // This function needs to go through (0,0) and (1,1), so we compute the
+    // values at 1 and 0, and then scale/shift them, respectively.
+    float offset = 1.0 / (1 + expf(slope * center));
+    float scale  = 1.0 / (1 + expf(slope * (center - 1))) - offset;
+
+    GLSL("// pl_shader_sigmoidize                               \n"
+         "color = clamp(color, 0.0, 1.0);                       \n"
+         "color = vec4("$") - vec4("$") *                       \n"
+         "    log(vec4(1.0) / (color * vec4("$") + vec4("$"))   \n"
+         "        - vec4(1.0));                                 \n",
+         SH_FLOAT(center), SH_FLOAT(1.0 / slope),
+         SH_FLOAT(scale), SH_FLOAT(offset));
+}
+
+

The advantage of this type of shader constant is that they will be +transparently replaced by dynamic uniforms whenever +pl_render_params.dynamic_constants is true, which allows the renderer to +respond more instantly to changes in the parameters (e.g. as a result of a user +dragging a slider around). During "normal" playback, they will then be +"promoted" to actual shader constants to prevent them from taking up registers.

+

Dynamic variables

+

For anything else, e.g. variables which are expected to change very frequently, +you can use the generic sh_var() mechanism, which sends constants either as +elements of a uniform buffer, or directly as push constants:

+
ident_t sh_var_int(pl_shader sh, const char *name, int val, bool dynamic);
+ident_t sh_var_uint(pl_shader sh, const char *name, unsigned int val, bool dynamic);
+ident_t sh_var_float(pl_shader sh, const char *name, float val, bool dynamic);
+#define SH_INT_DYN(val)   sh_var_int(sh, "const", val, true)
+#define SH_UINT_DYN(val)  sh_var_uint(sh, "const", val, true)
+#define SH_FLOAT_DYN(val) sh_var_float(sh, "const", val, true)
+
+

These are used primarily when a variable is expected to change very frequently, +e.g. as a result of randomness, or for constants which depend on dynamically +computed, source-dependent variables (e.g. input frame characteristics):

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
if (params->show_clipping) {
+    const float eps = 1e-6f;
+    GLSL("bool clip_hi, clip_lo;                            \n"
+         "clip_hi = any(greaterThan(color.rgb, vec3("$"))); \n"
+         "clip_lo = any(lessThan(color.rgb, vec3("$")));    \n"
+         "clip_hi = clip_hi || ipt.x > "$";                 \n"
+         "clip_lo = clip_lo || ipt.x < "$";                 \n",
+         SH_FLOAT_DYN(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, tone.input_max) + eps),
+         SH_FLOAT(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, tone.input_min) - eps),
+         SH_FLOAT_DYN(tone.input_max + eps),
+         SH_FLOAT(tone.input_min - eps));
+}
+
+

Shader sections (GLSL, GLSLH, GLSLF)

+

Shader macros come in three main flavors, depending on where the resulting text +should be formatted:

+
    +
  • GLSL: Expanded in the scope of the current main function, + and is related to code directly processing the current pixel value.
  • +
  • GLSLH: Printed to the 'header', before the first function, but after + variables, uniforms etc. This is used for global definitions, helper + functions, shared memory variables, and so on.
  • +
  • GLSLF: Printed to the footer, which is always at the end of the current + main function, but before returning to the caller / writing to the + framebuffer. Used to e.g. update SSBO state in preparation for the next + frame.
  • +
+

Finally, there is a fourth category GLSLP (prelude), which is currently only +used internally to generate preambles during e.g. compute shader translation.

+

New #pragma GLSL macro

+

Starting with libplacebo v6, the internal shader system has been augmented by a +custom macro preprocessor, which is designed to ease the boilerplate of writing +shaders (and also strip redundant whitespace from generated shaders). The code +for this is found in the +tools/glsl_preproc +directory.

+

In a nutshell, this allows us to embed GLSL snippets directly as #pragma GLSL +macros (resp. #pragma GLSLH, #pragma GLSLF):

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
bool pl_shader_sample_bicubic(pl_shader sh, const struct pl_sample_src *src)
+{
+    ident_t tex, pos, pt;
+    float rx, ry, scale;
+    if (!setup_src(sh, src, &tex, &pos, &pt, &rx, &ry, NULL, &scale, true, LINEAR))
+        return false;
+
+    if (rx < 1 || ry < 1) {
+        PL_TRACE(sh, "Using fast bicubic sampling when downscaling. This "
+                 "will most likely result in nasty aliasing!");
+    }
+
+    // Explanation of how bicubic scaling with only 4 texel fetches is done:
+    //   http://www.mate.tue.nl/mate/pdfs/10318.pdf
+    //   'Efficient GPU-Based Texture Interpolation using Uniform B-Splines'
+
+    sh_describe(sh, "bicubic");
+#pragma GLSL /* pl_shader_sample_bicubic */         \
+    vec4 color;                                     \
+    {                                               \
+    vec2 pos = $pos;                                \
+    vec2 size = vec2(textureSize($tex, 0));         \
+    vec2 frac  = fract(pos * size + vec2(0.5));     \
+    vec2 frac2 = frac * frac;                       \
+    vec2 inv   = vec2(1.0) - frac;                  \
+    vec2 inv2  = inv * inv;                         \
+    /* compute basis spline */                      \
+    vec2 w0 = 1.0/6.0 * inv2 * inv;                 \
+    vec2 w1 = 2.0/3.0 - 0.5 * frac2 * (2.0 - frac); \
+    vec2 w2 = 2.0/3.0 - 0.5 * inv2  * (2.0 - inv);  \
+    vec2 w3 = 1.0/6.0 * frac2 * frac;               \
+    vec4 g = vec4(w0 + w1, w2 + w3);                \
+    vec4 h = vec4(w1, w3) / g + inv.xyxy;           \
+    h.xy -= vec2(2.0);                              \
+    /* sample four corners, then interpolate */     \
+    vec4 p = pos.xyxy + $pt.xyxy * h;               \
+    vec4 c00 = textureLod($tex, p.xy, 0.0);         \
+    vec4 c01 = textureLod($tex, p.xw, 0.0);         \
+    vec4 c0 = mix(c01, c00, g.y);                   \
+    vec4 c10 = textureLod($tex, p.zy, 0.0);         \
+    vec4 c11 = textureLod($tex, p.zw, 0.0);         \
+    vec4 c1 = mix(c11, c10, g.y);                   \
+    color = ${float:scale} * mix(c1, c0, g.x);      \
+    }
+
+    return true;
+}
+
+

This gets transformed, by the GLSL macro preprocessor, into an optimized shader +template invocation like the following:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
{
+    // ...
+    sh_describe(sh, "bicubic");
+    const struct __attribute__((__packed__)) {
+        ident_t pos;
+        ident_t tex;
+        ident_t pt;
+        ident_t scale;
+    } _glsl_330_args = {
+        .pos = pos,
+        .tex = tex,
+        .pt = pt,
+        .scale = sh_const_float(sh, "scale", scale),
+    };
+    size_t _glsl_330_fn(void *, pl_str *, const uint8_t *);
+    pl_str_builder_append(sh->buffers[SH_BUF_BODY], _glsl_330_fn,
+                          &_glsl_330_args, sizeof(_glsl_330_args));
+    // ...
+}
+
+size_t _glsl_330_fn(void *alloc, pl_str *buf, const uint8_t *ptr)
+{
+    struct __attribute__((__packed__)) {
+        ident_t pos;
+        ident_t tex;
+        ident_t pt;
+        ident_t scale;
+    } vars;
+    memcpy(&vars, ptr, sizeof(vars));
+
+    pl_str_append_asprintf(alloc, buf,
+        "/* pl_shader_sample_bicubic */\n"
+        "    vec4 color;\n"
+        "    {\n"
+        "    vec2 pos = /*pos*/_%hx;\n"
+        "    vec2 size = vec2(textureSize(/*tex*/_%hx, 0));\n"
+        "    vec2 frac  = fract(pos * size + vec2(0.5));\n"
+        "    vec2 frac2 = frac * frac;\n"
+        "    vec2 inv   = vec2(1.0) - frac;\n"
+        "    vec2 inv2  = inv * inv;\n"
+        "    /* compute basis spline */\n"
+        "    vec2 w0 = 1.0/6.0 * inv2 * inv;\n"
+        "    vec2 w1 = 2.0/3.0 - 0.5 * frac2 * (2.0 - frac);\n"
+        "    vec2 w2 = 2.0/3.0 - 0.5 * inv2  * (2.0 - inv);\n"
+        "    vec2 w3 = 1.0/6.0 * frac2 * frac;\n"
+        "    vec4 g = vec4(w0 + w1, w2 + w3);\n"
+        "    vec4 h = vec4(w1, w3) / g + inv.xyxy;\n"
+        "    h.xy -= vec2(2.0);\n"
+        "    /* sample four corners, then interpolate */\n"
+        "    vec4 p = pos.xyxy + /*pt*/_%hx.xyxy * h;\n"
+        "    vec4 c00 = textureLod(/*tex*/_%hx, p.xy, 0.0);\n"
+        "    vec4 c01 = textureLod(/*tex*/_%hx, p.xw, 0.0);\n"
+        "    vec4 c0 = mix(c01, c00, g.y);\n"
+        "    vec4 c10 = textureLod(/*tex*/_%hx, p.zy, 0.0);\n"
+        "    vec4 c11 = textureLod(/*tex*/_%hx, p.zw, 0.0);\n"
+        "    vec4 c1 = mix(c11, c10, g.y);\n"
+        "    color = /*scale*/_%hx * mix(c1, c0, g.x);\n"
+        "    }\n",
+        vars.pos,
+        vars.tex,
+        vars.pt,
+        vars.tex,
+        vars.tex,
+        vars.tex,
+        vars.tex,
+        vars.scale
+    );
+
+    return sizeof(vars);
+}
+
+

To support this style of shader programming, special syntax was invented:

+

Shader variables

+

Instead of being formatted with "$", %f etc. and supplied in a big list, +printf style, GLSL macros may directly embed shader variables:

+
ident_t pos, tex = sh_bind(sh, texture, ..., &pos, ...);
+#pragma GLSL vec4 color = texture($tex, $pos);
+
+

The simplest possible shader variable is just $name, which corresponds to +any variable of type ident_t. More complicated expression are also possible:

+
#define RAND3 ${sh_prng(sh, false, NULL)}
+color.rgb += ${float:params->noise} * RAND3;
+
+

In the expression ${float:params->noise}, the float: prefix here transforms +the shader variable into the equivalent of SH_FLOAT() in the legacy API, +that is, a generic float (specialization) constant. Other possible types are:

+
float f = ${float: M_PI};
+int   i = ${int:   params->width};
+uint  u = ${uint:  sizeof(ssbo)};
+
+

In addition to a type specifier, the optional qualifiers dynamic and const +will modify the variable, turning it into (respectively) a dynamically loaded +uniform (SH_FLOAT_DYN etc.), or a hard-coded shader literal (%d, %f +etc.):

+
const float base = ${const float: M_LOG10E};
+int seed = ${dynamic int: rand()};
+
+

Macro directives

+

Lines beginning with @ are not included in the GLSL as-is, but instead parsed +as macro directives, to control the code flow inside the macro expansion:

+

@if / @else

+

Standard-purpose conditional. Example:

+
float alpha = ...;
+@if (repr.alpha == PL_ALPHA_INDEPENDENT)
+    color.a *= alpha;
+@else
+    color.rgba *= alpha;
+
+

The condition is evaluated outside the macro (in the enclosing scope) and +the resulting boolean variable is directly passed to the template.

+

An @if block can also enclose multiple lines:

+
@if (threshold > 0) {
+    float thresh = ${float:threshold};
+    coeff = mix(coeff, vec2(0.0), lessThan(coeff, vec2(thresh)));
+    coeff = mix(coeff, vec2(1.0), greaterThan(coeff, vec2(1.0 - thresh)));
+@}
+
+

@for

+

This can be used to generate (unrolled) loops:

+
int offset = ${const int: params->kernel_width / 2};
+float sum = 0.0;
+@for (x < params->kernel_width)
+    sum += textureLodOffset($luma, $pos, 0.0, int(@sum - offset)).r;
+
+

This introduces a local variable, @x, which expands to an integer containing +the current loop index. Loop indices always start at 0. Valid terminating +conditions include < and <=, and the loop stop condition is also evaluated +as an integer.

+

Alternatively, this can be used to iterate over a bitmask (as commonly used for +e.g. components in a color mask):

+
float weight = /* ... */;
+vec4 color = textureLod($tex, $pos, 0.0);
+@for (c : params->component_mask)
+    sum[@c] += weight * color[@c];
+
+

Finally, to combine loops with conditionals, the special syntax @if @(cond) +may be used to evaluate expressions inside the template loop:

+
@for (i < 10) {
+    float weight = /* ... */;
+    @if @(i < 5)
+        weight = -weight;
+    sum += weight * texture(...);
+@}
+
+

In this case, the @if conditional may only reference local (loop) variables.

+

@switch / @case

+

This corresponds fairly straightforwardly to a normal switch/case from C:

+
@switch (color->transfer) {
+@case PL_COLOR_TRC_SRGB:
+    color.rgb = mix(color.rgb * 1.0/12.92,
+                    pow((color.rgb + vec3(0.055)) / 1.055, vec3(2.4)),
+                    lessThan(vec3(0.04045), color.rgb));
+    @break;
+@case PL_COLOR_TRC_GAMMA18:
+    color.rgb = pow(color.rgb, vec3(1.8));
+    @break;
+@case PL_COLOR_TRC_GAMMA20:
+    color.rgb = pow(color.rgb, vec3(2.0));
+    @break;
+@case PL_COLOR_TRC_GAMMA22:
+    color.rgb = pow(color.rgb, vec3(2.2));
+    @break;
+/* ... */
+@}
+
+

The switch body is always evaluated as an unsigned int.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..734dcb57 --- /dev/null +++ b/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + Introduction - libplacebo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Introduction

+

Overview

+

This document will serve as an introduction to and usage example for the +libplacebo API. This is not +intended as a full API reference, for that you should see the repository of +header +files, +which are written to be (hopefully) understandable as-is.

+

libplacebo exposes large parts of its internal abstractions publicly. This +guide will take the general approach of starting as high level as possible and +diving into the details in later chapters.

+

A full listing of currently available APIs and their corresponding header +files can be seen +here.

+

Getting Started

+

To get started using libplacebo, you need to install it (and its development +headers) somehow onto your system. On most distributions, this should be as +simple as installing the corresponding libplacebo-devel package, or the +appropriate variants.

+

You can see a fill list of libplacebo packages and their names on +repology.

+
+

API versions

+

This document is targeting the "v4 API" overhaul, and as such, examples +provided will generally fail to compile on libplacebo versions below v4.x.

+
+

Alternatively, you can install it from the source code. For that, see the +build instructions located here.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/renderer/index.html b/renderer/index.html new file mode 100644 index 00000000..23575117 --- /dev/null +++ b/renderer/index.html @@ -0,0 +1,1001 @@ + + + + + + + + + + + + + + + + + + + + + + + + Rendering content: pl_frame, pl_renderer, and pl_queue - libplacebo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Rendering content: pl_frame, pl_renderer, and pl_queue

+

This example roughly builds off the previous entry, +and as such will not cover the basics of how to create a window, initialize a +pl_gpu and get pixels onto the screen.

+

Renderer

+

The pl_renderer set of APIs represents the highest-level interface into +libplacebo, and is what most users who simply want to display e.g. a video +feed on-screen will want to be using.

+

The basic initialization is straightforward, requiring no extra parameters:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
pl_renderer renderer;
+
+init()
+{
+    renderer = pl_renderer_create(pllog, gpu);
+    if (!renderer)
+        goto error;
+
+    // ...
+}
+
+uninit()
+{
+    pl_renderer_destroy(&renderer);
+}
+
+

What makes the renderer powerful is the large number of pl_render_params it +exposes. By default, libplacebo provides several presets to use:

+
    +
  • pl_render_fast_params: Disables everything except for defaults. This is + the fastest possible configuration.
  • +
  • pl_render_default_params: Contains the recommended default parameters, + including some slightly higher quality scaling, as well as dithering.
  • +
  • pl_render_high_quality_params: A preset of reasonable defaults for a + higher-end machine (i.e. anything with a discrete GPU). This enables most + of the basic functionality, including upscaling, downscaling, debanding + and better HDR tone mapping.
  • +
+

Covering all of the possible options exposed by pl_render_params is +out-of-scope of this example and would be better served by looking at the API +documentation.

+

Frames

+

pl_frame +is the struct libplacebo uses to group textures and their metadata together +into a coherent unit that can be rendered using the renderer. This is not +currently a dynamically allocated or refcounted heap object, it is merely a +struct that can live on the stack (or anywhere else). The actual data lives in +corresponding pl_tex objects referenced in each of the frame's planes.

+
1
+2
+3
+4
+5
+6
+7
+8
+9
bool render_frame(const struct pl_frame *image,
+                  const struct pl_swapchain_frame *swframe)
+{
+    struct pl_frame target;
+    pl_frame_from_swapchain(&target, swframe);
+
+    return pl_render_image(renderer, image, target,
+                           &pl_render_default_params);
+}
+
+
+

Renderer state

+

The pl_renderer is conceptually (almost) stateless. The only thing that +is needed to get a different result is to change the render params, which +can be varied freely on every call, if the user desires.

+

The one case where this is not entirely true is when using frame mixing +(see below), or when using HDR peak detection. In this case, the renderer +can be explicitly reset using pl_renderer_flush_cache.

+
+

To upload frames, the easiest methods are made available as dedicated helpers +in +<libplacebo/utils/upload.h>, +and +<libplacebo/utils/libav.h> +(for AVFrames). In general, I recommend checking out the demo +programs +for a clearer illustration of how to use them in practice.

+

Shader cache

+

The renderer internally generates, compiles and caches a potentially large +number of shader programs, some of which can be complex. On some platforms +(notably D3D11), these can be quite costly to recompile on every program +launch.

+

As such, the renderer offers a way to save/restore its internal shader cache +from some external location (managed by the API user). The use of this API is +highly recommended:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
static uint8_t *load_saved_cache();
+static void store_saved_cache(uint8_t *cache, size_t bytes);
+
+void init()
+{
+    renderer = pl_renderer_create(pllog, gpu);
+    if (!renderer)
+        goto error;
+
+    uint8_t *cache = load_saved_cache();
+    if (cache) {
+        pl_renderer_load(renderer, cache);
+        free(cache);
+    }
+
+    // ...
+}
+
+void uninit()
+{
+    size_t cache_bytes = pl_renderer_save(renderer, NULL);
+    uint8_t *cache = malloc(cache_bytes);
+    if (cache) {
+        pl_renderer_save(renderer, cache);
+        store_saved_cache(cache, cache_bytes);
+        free(cache);
+    }
+
+    pl_renderer_destroy(&renderer);
+}
+
+
+

Cache safety

+

libplacebo performs only minimal validity checking on the shader cache, +and in general, cannot possibly guard against malicious alteration of such +files. Loading a cache from an untrusted source represents a remote code +execution vector.

+
+

Frame mixing

+

One of the renderer's most powerful features is its ability to compensate +for differences in framerates between the source and display by using frame +mixing to blend +adjacent frames together.

+

Using this API requires presenting the renderer, at each vsync, with a +pl_frame_mix struct, describing the current state of the vsync. In +principle, such structs can be constructed by hand. To do this, all of the +relevant frames (nearby the vsync timestamp) must be collected, and their +relative distances to the vsync determined, by normalizing all PTS values such +that the vsync represents time 0.0 (and a distance of 1.0 represents the +nominal duration between adjacent frames). Note that timing vsyncs, and +determining the correct vsync duration, are both left as problems for the user +to solve.1. Here could be an example of a valid struct:

+
(struct pl_frame_mix) {
+    .num_frames = 6
+    .frames = (const struct pl_frame *[]) {
+        /* frame 0 */
+        /* frame 1 */
+        /* ... */
+        /* frame 5 */
+    },
+    .signatures = (uint64_t[]) {
+        0x0, 0x1, 0x2, 0x3, 0x4, 0x5 // (1)
+    },
+    .timestamps = (float[]) {
+        -2.4, -1.4, -0.4, 0.6, 1.6, 2.6, // (2)
+    },
+    .vsync_duration = 0.4, // 24 fps video on 60 fps display
+}
+
+
    +
  1. +

    These must be unique per frame, but always refer to the same frame. For + example, this could be based on the frame's PTS, the frame's numerical ID + (in order of decoding), or some sort of hash. The details don't matter, + only that this uniquely identifies specific frames.

    +
  2. +
  3. +

    Typically, for CFR sources, frame timestamps will always be separated in + this list by a distance of 1.0. In this example, the vsync falls roughly + halfway (but not quite) in between two adjacent frames (with IDs 0x2 and + 0x3).

    +
  4. +
+
+

Frame mixing radius

+

In this example, the frame mixing radius (as determined by +pl_frame_mix_radius is 3.0, so we include all frames that fall within +the timestamp interval of [-3, 3). In general, you should consult this +function to determine what frames need to be included in the +pl_frame_mix - though including more frames than needed is not an error.

+
+

Frame queue

+

Because this API is rather unwieldy and clumsy to use directly, libplacebo +provides a helper abstraction known as pl_queue to assist in transforming +some arbitrary source of frames (such as a video decoder) into nicely packed +pl_frame_mix structs ready for consumption by the pl_renderer:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
#include <libplacebo/utils/frame_queue.h>
+
+pl_queue queue;
+
+void init()
+{
+    queue = pl_queue_create(gpu);
+}
+
+void uninit()
+{
+    pl_queue_destroy(&queue);
+    // ...
+}
+
+

This queue can be interacted with through a number of mechanisms: either +pushing frames (blocking or non-blocking), or by having the queue poll frames +(via blocking or non-blocking callback) as-needed. For a full overview of the +various methods of pushing and polling frames, check the API +documentation.

+

In this example, I will assume that we have a separate decoder thread pushing +frames into the pl_queue in a blocking manner:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
static void decoder_thread(void)
+{
+    void *frame;
+
+    while ((frame = /* decode new frame */)) {
+        pl_queue_push_block(queue, UINT64_MAX, &(struct pl_source_frame) {
+            .pts        = /* frame pts */,
+            .duration   = /* frame duration */,
+            .map        = /* map callback */,
+            .unmap      = /* unmap callback */,
+            .frame_data = frame,
+        });
+    }
+
+    pl_queue_push(queue, NULL); // signal EOF
+}
+
+

Now, in our render loop, we want to call pl_queue_update with appropriate +values to retrieve the correct frame mix for each vsync:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
bool render_frame(const struct pl_swapchain_frame *swframe)
+{
+    struct pl_frame_mix mix;
+    enum pl_queue_status res;
+    res = pl_queue_update(queue, &mix, pl_queue_params(
+        .pts            = /* time of next vsync */,
+        .radius         = pl_frame_mix_radius(&render_params),
+        .vsync_duration = /* if known */,
+        .timeout        = UINT64_MAX, // (2)
+    ));
+
+    switch (res) {
+    case PL_QUEUE_OK:
+        break;
+    case PL_QUEUE_EOF:
+        /* no more frames */
+        return false;
+    case PL_QUEUE_ERR:
+        goto error;
+    // (1)
+    }
+
+
+    struct pl_frame target;
+    pl_frame_from_swapchain(&target, swframe);
+
+    return pl_render_image_mix(renderer, &mix, target,
+                               &pl_render_default_params);
+}
+
+
    +
  1. +

    There is a fourth status, PL_QUEUE_MORE, which is returned only if the + resulting frame mix is incomplete (and the timeout was reached) - + basically this can only happen if the queue runs dry due to frames not + being supplied fast enough.

    +

    In this example, since we are setting timeout to UINT64_MAX, we will +never get this return value.

    +
  2. +
  3. +

    Setting this makes pl_queue_update block indefinitely until sufficiently + many frames have been pushed into the pl_queue from our separate + decoding thread.

    +
  4. +
+

Deinterlacing

+

The frame queue also vastly simplifies the process of performing +motion-adaptive temporal deinterlacing, by automatically linking together +adjacent fields/frames. To take advantage of this, all you need to do is set +the appropriate field (pl_source_frame.first_frame), as well as enabling +deinterlacing +parameters.

+
+
+
    +
  1. +

    However, this may change in the future, as the recent introduction of + the Vulkan display timing extension may result in display timing feedback + being added to the pl_swapchain API. That said, as of writing, this has + not yet happened. 

    +
  2. +
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 00000000..51343fe3 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Introduction","text":""},{"location":"#overview","title":"Overview","text":"

This document will serve as an introduction to and usage example for the libplacebo API. This is not intended as a full API reference, for that you should see the repository of header files, which are written to be (hopefully) understandable as-is.

libplacebo exposes large parts of its internal abstractions publicly. This guide will take the general approach of starting as high level as possible and diving into the details in later chapters.

A full listing of currently available APIs and their corresponding header files can be seen here.

"},{"location":"#getting-started","title":"Getting Started","text":"

To get started using libplacebo, you need to install it (and its development headers) somehow onto your system. On most distributions, this should be as simple as installing the corresponding libplacebo-devel package, or the appropriate variants.

You can see a fill list of libplacebo packages and their names on repology.

API versions

This document is targeting the \"v4 API\" overhaul, and as such, examples provided will generally fail to compile on libplacebo versions below v4.x.

Alternatively, you can install it from the source code. For that, see the build instructions located here.

"},{"location":"basic-rendering/","title":"Basic windowing / output example","text":"

We will demonstrate the basics of the libplacebo GPU output API with a worked example. The goal is to show a simple color on screen.

"},{"location":"basic-rendering/#creating-a-pl_log","title":"Creating a pl_log","text":"

Almost all major entry-points into libplacebo require providing a log callback (or NULL to disable logging). This is abstracted into the pl_log object type, which we can create with pl_log_create:

#include <libplacebo/log.h>\npl_log pllog;\nint main()\n{\npllog = pl_log_create(PL_API_VER, pl_log_params(\n.log_cb = pl_log_color,\n.log_level = PL_LOG_INFO,\n));\n// ...\npl_log_destroy(&pllog);\nreturn 0;\n}\n

Compiling

You can compile this example with:

$ gcc example.c -o example `pkg-config --cflags --libs libplacebo`\n

The parameter PL_API_VER has no special significance and is merely included for historical reasons. Aside from that, this snippet introduces a number of core concepts of the libplacebo API:

"},{"location":"basic-rendering/#parameter-structs","title":"Parameter structs","text":"

For extensibility, almost all libplacebo calls take a pointer to a const struct pl_*_params, into which all extensible parameters go. For convenience, libplacebo provides macros which create anonymous params structs on the stack (and also fill in default parameters). Note that this only works for C99 and above, users of C89 and C++ must initialize parameter structs manually.

Under the hood, pl_log_params(...) just translates to &((struct pl_log_params) { /* default params */, ... }). This style of API allows libplacebo to effectively simulate optional named parameters.

On default parameters

Wherever possible, parameters are designed in such a way that {0} gives you a minimal parameter structure, with default behavior and no optional features enabled. This is done for forwards compatibility - as new features are introduced, old struct initializers will simply opt out of them.

"},{"location":"basic-rendering/#destructors","title":"Destructors","text":"

All libplacebo objects must be destroyed manually using the corresponding pl_*_destroy call, which takes a pointer to the variable the object is stored in. The resulting variable is written to NULL. This helps prevent use-after-free bugs.

NULL

As a general rule, all libplacebo destructors are safe to call on variables containing NULL. So, users need not explicitly NULL-test before calling destructors on variables.

"},{"location":"basic-rendering/#creating-a-window","title":"Creating a window","text":"

While libplacebo can work in isolation, to render images offline, for the sake of this guide we want to provide something graphical on-screen. As such, we need to create some sort of window. Libplacebo provides no built-in mechanism for this, it assumes the API user will already have a windowing system in-place.

Complete examples (based on GLFW and SDL) can be found in the libplacebo demos. But for now, we will focus on getting a very simple window on-screen using GLFW:

// ...\n#include <GLFW/glfw3.h>\nconst char * const title = \"libplacebo demo\";\nint width = 800;\nint height = 600;\nGLFWwindow *window;\nint main()\n{\npllog = pl_log_create(PL_API_VER, pl_log_params(\n.log_level = PL_LOG_INFO,\n));\nif (!glfwInit())\nreturn 1;\nwindow = glfwCreateWindow(width, height, title, NULL, NULL);\nif (!window)\nreturn 1;\nwhile (!glfwWindowShouldClose(window)) {\nglfwWaitEvents();\n}\nglfwDestroyWindow(window);\nglfwTerminate();\npl_log_destroy(&pllog);\nreturn 0;\n}\n

Compiling

We now also need to include the glfw3 library to compile this example.

$ gcc example.c -o example `pkg-config --cflags --libs glfw3 libplacebo`\n
"},{"location":"basic-rendering/#creating-the-pl_gpu","title":"Creating the pl_gpu","text":"

All GPU operations are abstracted into an internal pl_gpu object, which serves as the primary entry-point to any sort of GPU interaction. This object cannot be created directly, but must be obtained from some graphical API: currently there are Vulkan, OpenGL or D3D11. A pl_gpu can be accessed from an API-specific object like pl_vulkan, pl_opengl and pl_d3d11.

In this guide, for simplicity, we will be using OpenGL, simply because that's what GLFW initializes by default.

// ...\npl_opengl opengl;\nstatic bool make_current(void *priv);\nstatic void release_current(void *priv);\nint main()\n{\n// ...\nwindow = glfwCreateWindow(width, height, title, NULL, NULL);\nif (!window)\nreturn 1;\nopengl = pl_opengl_create(pllog, pl_opengl_params(\n.get_proc_addr      = glfwGetProcAddress,\n.allow_software     = true,         // allow software rasterers\n.debug              = true,         // enable error reporting\n.make_current       = make_current, // (1)\n.release_current    = release_current,\n));\nif (!opengl)\nreturn 2;\nwhile (!glfwWindowShouldClose(window)) {\nglfwWaitEvents();\n}\npl_opengl_destroy(&opengl);\nglfwDestroyWindow(window);\nglfwTerminate();\npl_log_destroy(&pllog);\nreturn 0;\n}\nstatic bool make_current(void *priv)\n{\nglfwMakeContextCurrent(window);\nreturn true;\n}\nstatic void release_current(void *priv)\n{\nglfwMakeContextCurrent(NULL);\n}\n
  1. Setting this allows the resulting pl_gpu to be thread-safe, which enables asynchronous transfers to be used. The alternative is to simply call glfwMakeContextCurrent once after creating the window.

    This method of making the context current is generally preferred, however, so we've demonstrated it here for completeness' sake.

"},{"location":"basic-rendering/#creating-a-swapchain","title":"Creating a swapchain","text":"

All access to window-based rendering commands are abstracted into an object known as a \"swapchain\" (from Vulkan terminology), including the default backbuffers on D3D11 and OpenGL. If we want to present something to screen, we need to first create a pl_swapchain.

We can use this swapchain to perform the equivalent of gl*SwapBuffers:

// ...\npl_swapchain swchain;\nstatic void resize_cb(GLFWwindow *win, int new_w, int new_h)\n{\nwidth  = new_w;\nheight = new_h;\npl_swapchain_resize(swchain, &width, &height);\n}\nint main()\n{\n// ...\nif (!opengl)\nreturn 2;\nswchain = pl_opengl_create_swapchain(opengl, pl_opengl_swapchain_params(\n.swap_buffers   = (void (*)(void *)) glfwSwapBuffers,\n.priv           = window,\n));\nif (!swchain)\nreturn 2;\n// (2)\nif (!pl_swapchain_resize(swchain, &width, &height))\nreturn 2;\nglfwSetFramebufferSizeCallback(window, resize_cb);\nwhile (!glfwWindowShouldClose(window)) {\npl_swapchain_swap_buffers(swchain);\nglfwPollEvents(); // (1)\n}\npl_swapchain_destroy(&swchain);\npl_opengl_destroy(&opengl);\nglfwDestroyWindow(window);\nglfwTerminate();\npl_log_destroy(&pllog);\nreturn 0;\n}\n
  1. We change this from glfwWaitEvents to glfwPollEvents because we now want to re-run our main loop once per vsync, rather than only when new events arrive. The pl_swapchain_swap_buffers call will ensure that this does not execute too quickly.

  2. The swapchain needs to be resized to fit the size of the window, which in GLFW is handled by listening to a callback. In addition to setting this callback, we also need to inform the swapchain of the initial window size.

    Note that the pl_swapchain_resize function handles both resize requests and size queries - hence, the actual swapchain size is returned back to the passed variables.

"},{"location":"basic-rendering/#getting-pixels-on-the-screen","title":"Getting pixels on the screen","text":"

With a swapchain in hand, we're now equipped to start drawing pixels to the screen:

// ...\nstatic void render_frame(struct pl_swapchain_frame frame)\n{\npl_gpu gpu = opengl->gpu;\npl_tex_clear(gpu, frame.fbo, (float[4]){ 1.0, 0.5, 0.0, 1.0 });\n}\nint main()\n{\n// ...\nwhile (!glfwWindowShouldClose(window)) {\nstruct pl_swapchain_frame frame;\nwhile (!pl_swapchain_start_frame(swchain, &frame))\nglfwWaitEvents(); // (1)\nrender_frame(frame);\nif (!pl_swapchain_submit_frame(swchain))\nbreak; // (2)\npl_swapchain_swap_buffers(swchain);\nglfwPollEvents();\n}\n// ...\n}\n
  1. If pl_swapchain_start_frame fails, it typically means the window is hidden, minimized or blocked. This is not a fatal condition, and as such we simply want to process window events until we can resume rendering.

  2. If pl_swapchain_submit_frame fails, it typically means the window has been lost, and further rendering commands are not expected to succeed. As such, in this case, we simply terminate the example program.

Our main render loop has changed into a combination of pl_swapchain_start_frame, rendering, and pl_swapchain_submit_frame. To start with, we simply use the pl_tex_clear function to blit a constant orange color to the framebuffer.

"},{"location":"basic-rendering/#interlude-rendering-commands","title":"Interlude: Rendering commands","text":"

The previous code snippet represented our first foray into the pl_gpu API. For more detail on this API, see the GPU API section. But as a general rule of thumb, all pl_gpu-level operations are thread safe, asynchronous (except when returning something to the CPU), and internally refcounted (so you can destroy all objects as soon as you no longer need the reference).

In the example loop, pl_swapchain_swap_buffers is the only operation that actually flushes commands to the GPU. You can force an early flush with pl_gpu_flush() or pl_gpu_finish(), but other than that, commands will \"queue\" internally and complete asynchronously at some unknown point in time, until forward progress is needed (e.g. pl_tex_download).

"},{"location":"basic-rendering/#conclusion","title":"Conclusion","text":"

We have demonstrated how to create a window, how to initialize the libplacebo API, create a GPU instance based on OpenGL, and how to write a basic rendering loop that blits a single color to the framebuffer.

Here is a complete transcript of the example we built in this section:

Basic rendering
#include <GLFW/glfw3.h>\n#include <libplacebo/log.h>\n#include <libplacebo/opengl.h>\n#include <libplacebo/gpu.h>\nconst char * const title = \"libplacebo demo\";\nint width = 800;\nint height = 600;\nGLFWwindow *window;\npl_log pllog;\npl_opengl opengl;\npl_swapchain swchain;\nstatic bool make_current(void *priv);\nstatic void release_current(void *priv);\nstatic void resize_cb(GLFWwindow *win, int new_w, int new_h)\n{\nwidth  = new_w;\nheight = new_h;\npl_swapchain_resize(swchain, &width, &height);\n}\nstatic void render_frame(struct pl_swapchain_frame frame)\n{\npl_gpu gpu = opengl->gpu;\npl_tex_clear(gpu, frame.fbo, (float[4]){ 1.0, 0.5, 0.0, 1.0 });\n}\nint main()\n{\npllog = pl_log_create(PL_API_VER, pl_log_params(\n.log_cb = pl_log_color,\n.log_level = PL_LOG_INFO,\n));\nif (!glfwInit())\nreturn 1;\nwindow = glfwCreateWindow(width, height, title, NULL, NULL);\nif (!window)\nreturn 1;\nopengl = pl_opengl_create(pllog, pl_opengl_params(\n.get_proc_addr      = glfwGetProcAddress,\n.allow_software     = true,         // allow software rasterers\n.debug              = true,         // enable error reporting\n.make_current       = make_current,\n.release_current    = release_current,\n));\nswchain = pl_opengl_create_swapchain(opengl, pl_opengl_swapchain_params(\n.swap_buffers   = (void (*)(void *)) glfwSwapBuffers,\n.priv           = window,\n));\nif (!swchain)\nreturn 2;\nif (!pl_swapchain_resize(swchain, &width, &height))\nreturn 2;\nglfwSetFramebufferSizeCallback(window, resize_cb);\nwhile (!glfwWindowShouldClose(window)) {\nstruct pl_swapchain_frame frame;\nwhile (!pl_swapchain_start_frame(swchain, &frame))\nglfwWaitEvents();\nrender_frame(frame);\nif (!pl_swapchain_submit_frame(swchain))\nbreak;\npl_swapchain_swap_buffers(swchain);\nglfwPollEvents();\n}\npl_swapchain_destroy(&swchain);\npl_opengl_destroy(&opengl);\nglfwDestroyWindow(window);\nglfwTerminate();\npl_log_destroy(&pllog);\nreturn 0;\n}\nstatic bool make_current(void *priv)\n{\nglfwMakeContextCurrent(window);\nreturn true;\n}\nstatic void release_current(void *priv)\n{\nglfwMakeContextCurrent(NULL);\n}\n
"},{"location":"custom-shaders/","title":"Custom Shaders (mpv .hook syntax)","text":"

libplacebo supports the same custom shader syntax used by mpv, with some important changes. This document will serve as a complete reference for this syntax.

"},{"location":"custom-shaders/#overview","title":"Overview","text":"

In general, user shaders are divided into distinct blocks. Each block can define a shader, a texture, a buffer, or a tunable parameter. Each block starts with a collection of header directives, which are lines starting with the syntax //!.

As an example, here is a simple shader that simply inverts the video signal:

//!HOOK LUMA\n//!HOOK RGB\n//!BIND HOOKED\nvec4 hook()\n{\nvec4 color = HOOKED_texOff(0);\ncolor.rgb = vec3(1.0) - color.rgb;\nreturn color;\n}\n

This shader defines one block - a shader block which hooks into the two texture stages LUMA and RGB, binds the hooked texture, inverts the value of the rgb channels, and then returns the modified color.

"},{"location":"custom-shaders/#expressions","title":"Expressions","text":"

In a few contexts, shader directives accept arithmetic expressions, denoted by <expr> in the listing below. For historical reasons, all expressions are given in reverse polish notation (RPN), and the only value type is a floating point number. The following value types and arithmetic operations are available:

  • 1.234: Literal float constant, evaluates to itself.
  • NAME.w, NAME.width: Evaluates to the width of a texture with name NAME.
  • NAME.h, NAME.height: Evaluates to the height of a texture with name NAME.
  • PAR: Evaluates to the value of a tunable shader parameter with name PAR.
  • +: Evaluates to X+Y.
  • -: Evaluates to X-Y.
  • *: Evaluates to X*Y.
  • /: Evaluates to X/Y.
  • %: Evaluates to fmod(X, Y).
  • >: Evaluates to (X > Y) ? 1.0 : 0.0.
  • <: Evaluates to (X < Y) ? 1.0 : 0.0.
  • =: Evaluates to fuzzy_eq(X, Y) ? 1.0 : 0.0, with some tolerance to allow for floating point inaccuracy. (Around 1 ppm)
  • !: Evaluates to X ? 0.0 : 1.0.

Note that + and * can be used as suitable replacements for the otherwise absent boolean logic expressions (|| and &&).

"},{"location":"custom-shaders/#shaders","title":"Shaders","text":"

Shaders are the default block type, and have no special syntax to indicate their presence. Shader stages contain raw GLSL code that will be (conditionally) executed. This GLSL snippet must define a single function vec4 hook(), or void hook() for compute shaders.

During the execution of any shader, the following global variables are made available:

  • int frame: A raw counter tracking the number of executions of this shader stage.
  • float random: A pseudo-random float uniformly distributed in the range [0,1).
  • vec2 input_size: The nominal size (in pixels) of the original input image.
  • vec2 target_size: The nominal size (in pixels) of the output rectangle.
  • vec2 tex_offset: The nominal offset (in pixels), of the original input crop.
  • vec4 linearize(vec4 color): Linearize the input color according to the image's tagged gamma function.
  • vec4 delinearize(vec4 color): Opposite counterpart to linearize.

Shader stages accept the following directives:

"},{"location":"custom-shaders/#hook-texture","title":"HOOK <texture>","text":"

A HOOK directive determines when a shader stage is run. During internal processing, libplacebo goes over a number of pre-defined hook points at set points in the processing pipeline. It is only possible to intercept the image, and run custom shaders, at these fixed hook points.

Here is a current list of hook points:

  • RGB: Input plane containing RGB values
  • LUMA: Input plane containing a Y value
  • CHROMA: Input plane containing chroma values (one or both)
  • ALPHA: Input plane containing a single alpha value
  • XYZ: Input plane containing XYZ values
  • CHROMA_SCALED: Chroma plane, after merging and upscaling to luma size
  • ALPHA_SCALED: Alpha plane, after upscaling to luma size
  • NATIVE: Merged input planes, before any sort of color conversion (as-is)
  • MAIN: After conversion to RGB, before linearization/scaling
  • LINEAR: After conversion to linear light (for scaling purposes)
  • SIGMOID: After conversion to sigmoidized light (for scaling purposes)
  • PREKERNEL: Immediately before the execution of the main scaler kernel
  • POSTKERNEL: Immediately after the execution of the main scaler kernel
  • SCALED: After scaling, in either linear or non-linear light RGB
  • PREOUTPUT: After color conversion to target colorspace, before alpha blending
  • OUTPUT: After alpha blending, before dithering and final output pass

MAINPRESUB

In mpv, MAIN and MAINPRESUB are separate shader stages, because the mpv option --blend-subtitles=video allows rendering overlays directly onto the pre-scaled video stage. libplacebo does not support this feature, and as such, the MAINPRESUB shader stage does not exist. It is still valid to refer to this name in shaders, but it is handled identically to MAIN.

It's possible for a hook point to never fire. For example, SIGMOID will not fire when downscaling, as sigmoidization only happens when upscaling. Similarly, LUMA/CHROMA will not fire on an RGB video and vice versa.

A single shader stage may hook multiple hook points simultaneously, for example, to cover both LUMA and RGB cases with the same logic. (See the example shader in the introduction)

"},{"location":"custom-shaders/#bind-texture","title":"BIND <texture>","text":"

The BIND directive makes a texture available for use in the shader. This can be any of the previously named hook points, a custom texture define by a TEXTURE block, a custom texture saved by a SAVE directive, or the special value HOOKED which allows binding whatever texture hook dispatched this shader stage.

A bound texture will define the following GLSL functions (as macros):

  • sampler2D NAME_raw: A reference to the raw texture sampler itself.
  • vec2 NAME_pos: The texel coordinates of the current pixel.
  • vec2 NAME_map(ivec2 id): A function that maps from gl_GlobalInvocationID to texel coordinates. (Compute shaders)
  • vec2 NAME_size: The size (in pixels) of the texture.
  • vec2 NAME_pt: Convenience macro for 1.0 / NAME_size. The size of a single pixel (in texel coordinates).
  • vec2 NAME_off: The sample offset of the texture. Basically, the pixel coordinates of the top-left corner of the sampled area.
  • float NAME_mul: The coefficient that must be multiplied into sampled values in order to rescale them to [0,1].
  • vec4 NAME_tex(vec2 pos): A wrapper around NAME_mul * textureLod(NAME_raw, pos, 0.0).
  • vec4 NAME_texOff(vec2 offset): A wrapper around NAME_tex(NAME_pos + NAME_pt * offset). This can be used to easily access adjacent pixels, e.g. NAME_texOff(-1,2) samples a pixel one to the left and two to the bottom of the current location.
  • vec4 NAME_gather(vec2 pos, int c): A wrapper around NAME_mul * textureGather(pos, c), with appropriate scaling. (Only when supported1)

Rotation matrix

For compatibility with mpv, we also define a mat2 NAME_rot which is simply equal to a 2x2 identity matrix. libplacebo never rotates input planes - all rotation happens during the final output to the display.

This same directive can also be used to bind buffer blocks (i.e. uniform/storage buffers), as defined by the BUFFER directive.

"},{"location":"custom-shaders/#save-texture","title":"SAVE <texture>","text":"

By default, after execution of a shader stage, the resulting output is captured back into the same hooked texture that triggered the shader. This behavior can be overridden using the explicit SAVE directive. For example, a shader might need access to a low-res version of the luma input texture in order to process chroma:

//!HOOK CHROMA\n//!BIND CHROMA\n//!BIND LUMA\n//!SAVE LUMA_LOWRES\n//!WIDTH CHROMA.w\n//!HEIGHT CHROMA.h\nvec4 hook()\n{\nreturn LUMA_texOff(0);\n}\n

This shader binds both luma and chroma and resizes the luma plane down to the size of the chroma plane, saving the result as a new texture LUMA_LOWRES. In general, you can pick any name you want, here.

"},{"location":"custom-shaders/#desc-description","title":"DESC <description>","text":"

This purely informative directive simply gives the shader stage a name. This is the name that will be reported to the shader stage and execution time metrics.

"},{"location":"custom-shaders/#offset-xo-yo-align","title":"OFFSET <xo yo | ALIGN>","text":"

This directive indicates a pixel shift (offset) introduced by this pass. These pixel offsets will be accumulated and corrected automatically as part of plane alignment / main scaling.

A special value of ALIGN will attempt to counteract any existing offset of the hooked texture by aligning it with reference plane (i.e. luma). This can be used to e.g. introduce custom chroma scaling in a way that doesn't break chroma subtexel offsets.

An example:

//!HOOK LUMA\n//!BIND HOOKED\n//!OFFSET 100.5 100.5\nvec4 hook()\n{\n// Constant offset by N pixels towards the bottom right\nreturn HOOKED_texOff(-vec2(100.5));\n}\n

This (slightly silly) shader simply shifts the entire sampled region to the bottom right by 100.5 pixels, and propagates this shift to the main scaler using the OFFSET directive. As such, the end result of this is that there is no visible shift of the overall image, but some detail (~100 pixels) near the bottom-right border is lost due to falling outside the bounds of the texture.

"},{"location":"custom-shaders/#width-expr-height-expr","title":"WIDTH <expr>, HEIGHT <expr>","text":"

These directives can be used to override the dimensions of the resulting texture. Note that not all textures can be resized this way. Currently, only RGB, LUMA, CHROMA, XYZ, NATIVE and MAIN are resizable. Trying to save a texture with an incompatible size to any other shader stage will result in an error.

"},{"location":"custom-shaders/#when-expr","title":"WHEN <expr>","text":"

This directive takes an expression that can be used to make shader stages conditionally executed. If this evaluates to 0, the shader stage will be skipped.

Example:

//!PARAM strength\n//!TYPE float\n//!MINIMUM 0\n1.0\n//!HOOK MAIN\n//!BIND HOOKED\n//!WHEN intensity 0 >\n//!DESC do something based on 'intensity'\n...\n

This example defines a shader stage that only conditionally executes itself if the value of the intensity shader parameter is non-zero.

"},{"location":"custom-shaders/#components-num","title":"COMPONENTS <num>","text":"

This directive overrides the number of components present in a texture. For example, if you want to extract a one-dimensional feature map from the otherwise 3 or 4 dimensional MAIN texture, you can use this directive to save on memory bandwidth and consumption by having libplacebo only allocate a one-component texture to store the feature map in:

//!HOOK MAIN\n//!BIND HOOKED\n//!SAVE featuremap\n//!COMPONENTS 1\n
"},{"location":"custom-shaders/#compute-bw-bh-tw-th","title":"COMPUTE <bw> <bh> [<tw> <th>]","text":"

This directive specifies that the shader should be treated as a compute shader, with the block size bw and bh. The compute shader will be dispatched with however many blocks are necessary to completely tile over the output. Within each block, there will be tw*th threads, forming a single work group. In other words: tw and th specify the work group size, which can be different from the block size. So for example, a compute shader with bw = bh = 32 and tw = th = 8 running on a 500x500 texture would dispatch 16x16 blocks (rounded up), each with 8x8 threads.

Instead of defining a vec4 hook(), compute shaders must define a void hook() which results directly to the output texture, a writeonly image2D out_image made available to the shader stage.

For example, here is a shader executing a single-pass 41x41 convolution (average blur) on the luma plane, using a compute shader to share sampling work between adjacent threads in a work group:

//!HOOK LUMA\n//!BIND HOOKED\n//!COMPUTE 32 32\n//!DESC avg convolution\n// Kernel size, 41x41 as an example\nconst ivec2 ksize = ivec2(41, 41);\nconst ivec2 offset = ksize / 2;\n// We need to load extra source texels to account for padding due to kernel\n// overhang\nconst ivec2 isize = ivec2(gl_WorkGroupSize) + ksize - 1;\nshared float inp[isize.y][isize.x];\nvoid hook()\n{\n// load texels into shmem\nivec2 base = ivec2(gl_WorkGroupID) * ivec2(gl_WorkGroupSize);\nfor (uint y = gl_LocalInvocationID.y; y < isize.y; y += gl_WorkGroupSize.y) {\nfor (uint x = gl_LocalInvocationID.x; x < isize.x; x += gl_WorkGroupSize.x)\ninp[y][x] = texelFetch(HOOKED_raw, base + ivec2(x,y) - offset, 0).x;\n}\n// synchronize threads\nbarrier();\n// do convolution\nfloat sum;\nfor (uint y = 0; y < ksize.y; y++) {\nfor (uint x = 0; x < ksize.x; x++)\nsum += inp[gl_LocalInvocationID.y+y][gl_LocalInvocationID.x+x];\n}\nvec4 color = vec4(HOOKED_mul * sum / (ksize.x * ksize.y), 0, 0, 1);\nimageStore(out_image, ivec2(gl_GlobalInvocationID), color);\n}\n
"},{"location":"custom-shaders/#textures","title":"Textures","text":"

Custom textures can be defined and made available to shader stages using TEXTURE blocks. These can be used to provide e.g. LUTs or pre-trained weights.

The data for a texture is provided as a raw hexadecimal string encoding the in-memory representation of a texture, according to its given texture format, for example:

//!TEXTURE COLORS\n//!SIZE 3 3\n//!FORMAT rgba32f\n//!FILTER NEAREST\n//!BORDER REPEAT\n0000803f000000000000000000000000000000000000803f00000000000000000000000\n0000000000000803f00000000000000000000803f0000803f000000000000803f000000\n000000803f000000000000803f0000803f00000000000000009a99993e9a99993e9a999\n93e000000009a99193F9A99193f9a99193f000000000000803f0000803f0000803f0000\n0000\n

Texture blocks accept the following directives:

"},{"location":"custom-shaders/#texture-name","title":"TEXTURE <name>","text":"

This must be the first directive in a texture block, and marks it as such. The name given is the name that the texture will be referred to (via BIND directives).

"},{"location":"custom-shaders/#size-width-height-depth","title":"SIZE <width> [<height> [<depth>]]","text":"

This directive gives the size of the texture, as integers. For example, //!SIZE 512 512 marks a 512x512 texture block. Textures can be 1D, 2D or 3D depending on the number of coordinates specified.

"},{"location":"custom-shaders/#format-fmt","title":"FORMAT <fmt>","text":"

This directive specifies the texture format. A complete list of known textures is exposed as part of the pl_gpu struct metadata, but they follow the format convention rgba8, rg16hf, rgba32f, r64i and so on.

"},{"location":"custom-shaders/#filter-linear-nearest","title":"FILTER <LINEAR | NEAREST>","text":"

This directive specifies the texture magnification/minification filter.

"},{"location":"custom-shaders/#border-clamp-repeat-mirror","title":"BORDER <CLAMP | REPEAT | MIRROR>","text":"

This directive specifies the border clamping method of the texture.

"},{"location":"custom-shaders/#storage","title":"STORAGE","text":"

If present, this directive marks the texture as a storage image. It will still be initialized with the initial values, but rather than being bound as a read-only and immutable sampler2D, it is bound as a readwrite coherent image2D. Such texture scan be used to, for example, store persistent state across invocations of the shader.

"},{"location":"custom-shaders/#buffers","title":"Buffers","text":"

Custom uniform / storage shader buffer blocks can be defined using BUFFER directives.

The (initial) data for a buffer is provided as a raw hexadecimal string encoding the in-memory representation of a buffer in the corresponding GLSL packing layout (std140 or std430 for uniform and storage blocks, respectively):

//!BUFFER buf_uniform\n//!VAR float foo\n//!VAR float bar\n0000000000000000\n//!BUFFER buf_storage\n//!VAR vec2 bat\n//!VAR int big[32];\n//!STORAGE\n

Buffer blocks accept the following directives:

"},{"location":"custom-shaders/#buffer-name","title":"BUFFER <name>","text":"

This must be the first directive in a buffer block, and marks it as such. The name given is mostly cosmetic, as individual variables can be accessed directly using the names given in the corresponding VAR directives.

"},{"location":"custom-shaders/#storage_1","title":"STORAGE","text":"

If present, this directive marks the buffer as a (readwrite coherent) shader storage block, instead of a readonly uniform buffer block. Such storage blocks can be used to track and evolve state across invocations of this shader.

Storage blocks may also be initialized with default data, but this is optional. They can also be initialized as part of the first shader execution (e.g. by testing for frame == 0).

"},{"location":"custom-shaders/#var-type-name","title":"VAR <type> <name>","text":"

This directive appends a new variable to the shader block, with GLSL type <type> and shader name <name>. For example, VAR float foo introduces a float foo; member into the buffer block, and VAR mat4 transform introduces a mat4 transform; member.

It is also possible to introduce array variables, using [N] as part of the variable name.

"},{"location":"custom-shaders/#tunable-parameters","title":"Tunable parameters","text":"

Finally, the PARAM directive allows introducing tunable shader parameters, which are exposed programmatically as part of the C API (pl_hook).2

The default value of a parameter is given as the block body, for example:

//!PARAM contrast\n//!DESC Gain to apply to image brightness\n//!TYPE float\n//!MINIMUM 0.0\n//!MAXIMUM 100.0\n1.0\n

Parameters accept the following directives:

"},{"location":"custom-shaders/#param-name","title":"PARAM <name>","text":"

This must be the first directive in a parameter block, and marks it as such. The name given is the name that will be used to refer to this parameter in GLSL code.

"},{"location":"custom-shaders/#desc-description_1","title":"DESC <description>","text":"

This directive can be used to provide a friendlier description of the shader parameter, exposed as part of the C API to end users.

"},{"location":"custom-shaders/#minimum-value-maximum-value","title":"MINIMUM <value>, MAXIMUM <value>","text":"

Provides the minimum/maximum value bound of this parameter. If absent, no minimum/maximum is enforced.

"},{"location":"custom-shaders/#type-enum-define-dynamic-constant-type","title":"TYPE [ENUM] <DEFINE | [DYNAMIC | CONSTANT] <type>>","text":"

This gives the type of the parameter, which determines what type of values it can hold and how it will be made available to the shader. <type> must be a scalar GLSL numeric type, such as int, float or uint.

If a type is ENUM, it is treated as an enumeration type. To use this, type must either be int or DEFINE. Instead of providing a single default value, the param body should be a list of all possible enumeration values (as separate lines). These names will be made available inside the shader body (as a #define), as well as inside RPN expressions (e.g. WHEN). The qualifiers MINIMUM and MAXIMUM are ignored for ENUM parameters, with the value range instead being set implicitly from the list of options.

The optional qualifiers DYNAMIC or CONSTANT mark the parameter as dynamically changing and compile-time constant, respectively. A DYNAMIC variable is assumed to change frequently, and will be grouped with other frequently-changing input parameters. A CONSTANT parameter will be introduced as a compile-time constant into the shader header, which means thy can be used in e.g. constant expressions such as array sizes.3

Finally, the special type TYPE DEFINE marks a variable as a preprocessor define, which can be used inside #if preprocessor expressions. For example:

//!PARAM taps\n//!DESC Smoothing taps\n//!TYPE DEFINE\n//!MINIMUM 0\n//!MAXIMUM 5\n2\n//!HOOK LUMA\n//!BIND HOOKED\nconst uint row_size = 2 * taps + 1;\nconst float weights[row_size] = {\n#if taps == 0\n1.0,\n#endif\n#if taps == 1\n0.10650697891920,\n0.78698604216159,\n0.10650697891920,\n#endif\n#if taps == 2\n0.05448868454964,\n0.24420134200323,\n0.40261994689424,\n0.24420134200323,\n0.05448868454964,\n#endif\n// ...\n};\n

An example of an enum parameter:

//!PARAM csp\n//!DESC Colorspace\n//!TYPE ENUM int\nBT709\nBT2020\nDCIP3\n//!HOOK MAIN\n//!BIND HOOKED\nconst mat3 matrices[3] = {\nmat3(...), // BT709\nmat3(...), // BT2020\nmat3(...), // DCIP3\n};\n#define MAT matrices[csp]\n// ...\n
"},{"location":"custom-shaders/#full-example","title":"Full example","text":"

A collection of full examples can be found in the mpv user shaders wiki, but here is an example of a parametrized Gaussian smoothed film grain compute shader:

//!PARAM intensity\n//!DESC Film grain intensity\n//!TYPE float\n//!MINIMUM 0\n0.1\n//!PARAM taps\n//!DESC Film grain smoothing taps\n//!TYPE DEFINE\n//!MINIMUM 0\n//!MAXIMUM 5\n2\n//!HOOK LUMA\n//!BIND HOOKED\n//!DESC Apply gaussian smoothed film grain\n//!WHEN intensity 0 >\n//!COMPUTE 32 32\nconst uint row_size = 2 * taps + 1;\nconst float weights[row_size] = {\n#if taps == 0\n1.0,\n#endif\n#if taps == 1\n0.10650697891920,\n0.78698604216159,\n0.10650697891920,\n#endif\n#if taps == 2\n0.05448868454964,\n0.24420134200323,\n0.40261994689424,\n0.24420134200323,\n0.05448868454964,\n#endif\n#if taps == 3\n0.03663284536919,\n0.11128075847888,\n0.21674532140370,\n0.27068214949642,\n0.21674532140370,\n0.11128075847888,\n0.03663284536919,\n#endif\n#if taps == 4\n0.02763055063889,\n0.06628224528636,\n0.12383153680577,\n0.18017382291138,\n0.20416368871516,\n0.18017382291138,\n0.12383153680577,\n0.06628224528636,\n0.02763055063889,\n#endif\n#if taps == 5\n0.02219054849244,\n0.04558899978527,\n0.07981140824009,\n0.11906462996609,\n0.15136080967773,\n0.16396720767670,\n0.15136080967773,\n0.11906462996609,\n0.07981140824009,\n0.04558899978527,\n0.02219054849244,\n#endif\n};\nconst uvec2 isize = uvec2(gl_WorkGroupSize) + uvec2(2 * taps);\nshared float grain[isize.y][isize.x];\n// PRNG\nfloat permute(float x)\n{\nx = (34.0 * x + 1.0) * x;\nreturn fract(x * 1.0/289.0) * 289.0;\n}\nfloat seed(uvec2 pos)\n{\nconst float phi = 1.61803398874989;\nvec3 m = vec3(fract(phi * vec2(pos)), random) + vec3(1.0);\nreturn permute(permute(m.x) + m.y) + m.z;\n}\nfloat rand(inout float state)\n{\nstate = permute(state);\nreturn fract(state * 1.0/41.0);\n}\n// Turns uniform white noise into gaussian white noise by passing it\n// through an approximation of the gaussian quantile function\nfloat rand_gaussian(inout float state) {\nconst float a0 = 0.151015505647689;\nconst float a1 = -0.5303572634357367;\nconst float a2 = 1.365020122861334;\nconst float b0 = 0.132089632343748;\nconst float b1 = -0.7607324991323768;\nfloat p = 0.95 * rand(state) + 0.025;\nfloat q = p - 0.5;\nfloat r = q * q;\nfloat g = q * (a2 + (a1 * r + a0) / (r*r + b1*r + b0));\ng *= 0.255121822830526; // normalize to [-1,1)\nreturn g;\n}\nvoid hook()\n{\n// generate grain in `grain`\nuint num_threads = gl_WorkGroupSize.x * gl_WorkGroupSize.y;\nfor (uint i = gl_LocalInvocationIndex; i < isize.y * isize.x; i += num_threads) {\nuvec2 pos = uvec2(i % isize.y, i / isize.y);\nfloat state = seed(gl_WorkGroupID.xy * gl_WorkGroupSize.xy + pos);\ngrain[pos.y][pos.x] = rand_gaussian(state);\n}\n// make writes visible\nbarrier();\n// convolve horizontally\nfor (uint y = gl_LocalInvocationID.y; y < isize.y; y += gl_WorkGroupSize.y) {\nfloat hsum = 0;\nfor (uint x = 0; x < row_size; x++) {\nfloat g = grain[y][gl_LocalInvocationID.x + x];\nhsum += weights[x] * g;\n}\n// update grain LUT\ngrain[y][gl_LocalInvocationID.x + taps] = hsum;\n}\nbarrier();\n// convolve vertically\nfloat vsum = 0.0;\nfor (uint y = 0; y < row_size; y++) {\nfloat g = grain[gl_LocalInvocationID.y + y][gl_LocalInvocationID.x + taps];\nvsum += weights[y] * g;\n}\nvec4 color = HOOKED_tex(HOOKED_pos);\ncolor.rgb += vec3(intensity * vsum);\nimageStore(out_image, ivec2(gl_GlobalInvocationID), color);\n}\n
  1. Because these are macros, their presence can be tested for using #ifdef inside the GLSL preprocessor.\u00a0\u21a9

  2. In mpv using --vo=gpu-next, these can be set using the --glsl-shader-opts option.\u00a0\u21a9

  3. On supported platforms, these are implemented using specialization constants, which can be updated at run-time without requiring a full shader recompilation.\u00a0\u21a9

"},{"location":"glsl/","title":"GLSL shader system","text":""},{"location":"glsl/#overall-design","title":"Overall design","text":"

Shaders in libplacebo are all written in GLSL, and built up incrementally, on demand. Generally, all shaders for each frame are generated per frame. So functions like pl_shader_color_map etc. are run anew for every frame. This makes the renderer very stateless and allows us to directly embed relevant constants, uniforms etc. as part of the same code that generates the actual GLSL shader.

To avoid this from becoming wasteful, libplacebo uses an internal string building abstraction (pl_str_builder). Rather than building up a string directly, a pl_str_builder is like a list of string building functions/callbacks to execute in order to generate the actual shader. Combined with an efficient pl_str_builder_hash, this allows us to avoid the bulk of the string templating work for already-cached shaders.

"},{"location":"glsl/#legacy-api","title":"Legacy API","text":"

For the vast majority of libplacebo's history, the main entry-point into the shader building mechanism was the GLSL() macro (and variants), which works like a printf-append:

void pl_shader_extract_features(pl_shader sh, struct pl_color_space csp)\n{\nif (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))\nreturn;\nsh_describe(sh, \"feature extraction\");\npl_shader_linearize(sh, &csp);\nGLSL(\"// pl_shader_extract_features             \\n\"\n\"{                                         \\n\"\n\"vec3 lms = %f * \"$\" * color.rgb;          \\n\"\n\"lms = pow(max(lms, 0.0), vec3(%f));       \\n\"\n\"lms = (vec3(%f) + %f * lms)               \\n\"\n\"        / (vec3(1.0) + %f * lms);         \\n\"\n\"lms = pow(lms, vec3(%f));                 \\n\"\n\"float I = dot(vec3(%f, %f, %f), lms);     \\n\"\n\"color = vec4(I, 0.0, 0.0, 1.0);           \\n\"\n\"}                                         \\n\",\nPL_COLOR_SDR_WHITE / 10000,\nSH_MAT3(pl_ipt_rgb2lms(pl_raw_primaries_get(csp.primaries))),\nPQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2,\npl_ipt_lms2ipt.m[0][0], pl_ipt_lms2ipt.m[0][1], pl_ipt_lms2ipt.m[0][2]);\n}\n

The special macro $ is a stand-in for an identifier (ident_t), which is the internal type used to pass references to loaded uniforms, descriptors and so on:

typedef unsigned short ident_t;\n#define $           \"_%hx\"\n#define NULL_IDENT  0u\n// ...\nident_t sh_var_mat3(pl_shader sh, const char *name, pl_matrix3x3 val);\n#define SH_MAT3(val) sh_var_mat3(sh, \"mat\", val)\n

In general, constants in libplacebo are divided into three categories:

"},{"location":"glsl/#literal-shader-constants","title":"Literal shader constants","text":"

These are values that are expected to change very infrequently (or never), or for which we want to generate a different shader variant per value. Such values should be directly formatted as numbers into the shader text: %d, %f and so on. This is commonly used for array sizes, constants that depend only on hardware limits, constants that never change (but which have a friendly name, like PQ_C2 above), and so on.

As an example, the debanding iterations weights are hard-coded like this, because the debanding shader is expected to change as a result of a different number of iterations anyway:

// For each iteration, compute the average at a given distance and\n// pick it instead of the color if the difference is below the threshold.\nfor (int i = 1; i <= params->iterations; i++) {\nGLSL(// Compute a random angle and distance\n\"d = \"$\".xy * vec2(%d.0 * \"$\", %f);    \\n\" // (1)\n\"d = d.x * vec2(cos(d.y), sin(d.y));   \\n\"\n// Sample at quarter-turn intervals around the source pixel\n\"avg = T(0.0);                         \\n\"\n\"avg += GET(+d.x, +d.y);               \\n\"\n\"avg += GET(-d.x, +d.y);               \\n\"\n\"avg += GET(-d.x, -d.y);               \\n\"\n\"avg += GET(+d.x, -d.y);               \\n\"\n\"avg *= 0.25;                          \\n\"\n// Compare the (normalized) average against the pixel\n\"diff = abs(res - avg);                \\n\"\n\"bound = T(\"$\" / %d.0);                \\n\",\nprng, i, radius, M_PI * 2,\nthreshold, i);\nif (num_comps > 1) {\nGLSL(\"res = mix(avg, res, greaterThan(diff, bound)); \\n\");\n} else {\nGLSL(\"res = mix(avg, res, diff > bound); \\n\");\n}\n}\n
  1. The %d.0 here corresponds to the iteration index i, while the %f corresponds to the fixed constant M_PI * 2.
"},{"location":"glsl/#specializable-shader-constants","title":"Specializable shader constants","text":"

These are used for tunable parameters that are expected to change infrequently during normal playback. These constitute by far the biggest category, and most parameters coming from the various _params structs should be loaded like this.

They are loaded using the sh_const_*() functions, which generate a specialization constant on supported platforms, falling back to a literal shader #define otherwise. For anoymous parameters, you can use the short-hands SH_FLOAT, SH_INT etc.:

ident_t sh_const_int(pl_shader sh, const char *name, int val);\nident_t sh_const_uint(pl_shader sh, const char *name, unsigned int val);\nident_t sh_const_float(pl_shader sh, const char *name, float val);\n#define SH_INT(val)     sh_const_int(sh, \"const\", val)\n#define SH_UINT(val)    sh_const_uint(sh, \"const\", val)\n#define SH_FLOAT(val)   sh_const_float(sh, \"const\", val)\n

Here is an example of them in action:

void pl_shader_sigmoidize(pl_shader sh, const struct pl_sigmoid_params *params)\n{\nif (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))\nreturn;\nparams = PL_DEF(params, &pl_sigmoid_default_params);\nfloat center = PL_DEF(params->center, 0.75);\nfloat slope  = PL_DEF(params->slope, 6.5);\n// This function needs to go through (0,0) and (1,1), so we compute the\n// values at 1 and 0, and then scale/shift them, respectively.\nfloat offset = 1.0 / (1 + expf(slope * center));\nfloat scale  = 1.0 / (1 + expf(slope * (center - 1))) - offset;\nGLSL(\"// pl_shader_sigmoidize                               \\n\"\n\"color = clamp(color, 0.0, 1.0);                       \\n\"\n\"color = vec4(\"$\") - vec4(\"$\") *                       \\n\"\n\"    log(vec4(1.0) / (color * vec4(\"$\") + vec4(\"$\"))   \\n\"\n\"        - vec4(1.0));                                 \\n\",\nSH_FLOAT(center), SH_FLOAT(1.0 / slope),\nSH_FLOAT(scale), SH_FLOAT(offset));\n}\n

The advantage of this type of shader constant is that they will be transparently replaced by dynamic uniforms whenever pl_render_params.dynamic_constants is true, which allows the renderer to respond more instantly to changes in the parameters (e.g. as a result of a user dragging a slider around). During \"normal\" playback, they will then be \"promoted\" to actual shader constants to prevent them from taking up registers.

"},{"location":"glsl/#dynamic-variables","title":"Dynamic variables","text":"

For anything else, e.g. variables which are expected to change very frequently, you can use the generic sh_var() mechanism, which sends constants either as elements of a uniform buffer, or directly as push constants:

ident_t sh_var_int(pl_shader sh, const char *name, int val, bool dynamic);\nident_t sh_var_uint(pl_shader sh, const char *name, unsigned int val, bool dynamic);\nident_t sh_var_float(pl_shader sh, const char *name, float val, bool dynamic);\n#define SH_INT_DYN(val)   sh_var_int(sh, \"const\", val, true)\n#define SH_UINT_DYN(val)  sh_var_uint(sh, \"const\", val, true)\n#define SH_FLOAT_DYN(val) sh_var_float(sh, \"const\", val, true)\n

These are used primarily when a variable is expected to change very frequently, e.g. as a result of randomness, or for constants which depend on dynamically computed, source-dependent variables (e.g. input frame characteristics):

if (params->show_clipping) {\nconst float eps = 1e-6f;\nGLSL(\"bool clip_hi, clip_lo;                            \\n\"\n\"clip_hi = any(greaterThan(color.rgb, vec3(\"$\"))); \\n\"\n\"clip_lo = any(lessThan(color.rgb, vec3(\"$\")));    \\n\"\n\"clip_hi = clip_hi || ipt.x > \"$\";                 \\n\"\n\"clip_lo = clip_lo || ipt.x < \"$\";                 \\n\",\nSH_FLOAT_DYN(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, tone.input_max) + eps),\nSH_FLOAT(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, tone.input_min) - eps),\nSH_FLOAT_DYN(tone.input_max + eps),\nSH_FLOAT(tone.input_min - eps));\n}\n
"},{"location":"glsl/#shader-sections-glsl-glslh-glslf","title":"Shader sections (GLSL, GLSLH, GLSLF)","text":"

Shader macros come in three main flavors, depending on where the resulting text should be formatted:

  • GLSL: Expanded in the scope of the current main function, and is related to code directly processing the current pixel value.
  • GLSLH: Printed to the 'header', before the first function, but after variables, uniforms etc. This is used for global definitions, helper functions, shared memory variables, and so on.
  • GLSLF: Printed to the footer, which is always at the end of the current main function, but before returning to the caller / writing to the framebuffer. Used to e.g. update SSBO state in preparation for the next frame.

Finally, there is a fourth category GLSLP (prelude), which is currently only used internally to generate preambles during e.g. compute shader translation.

"},{"location":"glsl/#new-pragma-glsl-macro","title":"New #pragma GLSL macro","text":"

Starting with libplacebo v6, the internal shader system has been augmented by a custom macro preprocessor, which is designed to ease the boilerplate of writing shaders (and also strip redundant whitespace from generated shaders). The code for this is found in the tools/glsl_preproc directory.

In a nutshell, this allows us to embed GLSL snippets directly as #pragma GLSL macros (resp. #pragma GLSLH, #pragma GLSLF):

bool pl_shader_sample_bicubic(pl_shader sh, const struct pl_sample_src *src)\n{\nident_t tex, pos, pt;\nfloat rx, ry, scale;\nif (!setup_src(sh, src, &tex, &pos, &pt, &rx, &ry, NULL, &scale, true, LINEAR))\nreturn false;\nif (rx < 1 || ry < 1) {\nPL_TRACE(sh, \"Using fast bicubic sampling when downscaling. This \"\n\"will most likely result in nasty aliasing!\");\n}\n// Explanation of how bicubic scaling with only 4 texel fetches is done:\n//   http://www.mate.tue.nl/mate/pdfs/10318.pdf\n//   'Efficient GPU-Based Texture Interpolation using Uniform B-Splines'\nsh_describe(sh, \"bicubic\");\n#pragma GLSL /* pl_shader_sample_bicubic */         \\\n    vec4 color;                                     \\\n    {                                               \\\n    vec2 pos = $pos;                                \\\n    vec2 size = vec2(textureSize($tex, 0));         \\\n    vec2 frac  = fract(pos * size + vec2(0.5));     \\\n    vec2 frac2 = frac * frac;                       \\\n    vec2 inv   = vec2(1.0) - frac;                  \\\n    vec2 inv2  = inv * inv;                         \\\n/* compute basis spline */                      \\\n    vec2 w0 = 1.0/6.0 * inv2 * inv;                 \\\n    vec2 w1 = 2.0/3.0 - 0.5 * frac2 * (2.0 - frac); \\\n    vec2 w2 = 2.0/3.0 - 0.5 * inv2  * (2.0 - inv);  \\\n    vec2 w3 = 1.0/6.0 * frac2 * frac;               \\\n    vec4 g = vec4(w0 + w1, w2 + w3);                \\\n    vec4 h = vec4(w1, w3) / g + inv.xyxy;           \\\n    h.xy -= vec2(2.0);                              \\\n/* sample four corners, then interpolate */     \\\n    vec4 p = pos.xyxy + $pt.xyxy * h;               \\\n    vec4 c00 = textureLod($tex, p.xy, 0.0);         \\\n    vec4 c01 = textureLod($tex, p.xw, 0.0);         \\\n    vec4 c0 = mix(c01, c00, g.y);                   \\\n    vec4 c10 = textureLod($tex, p.zy, 0.0);         \\\n    vec4 c11 = textureLod($tex, p.zw, 0.0);         \\\n    vec4 c1 = mix(c11, c10, g.y);                   \\\n    color = ${float:scale} * mix(c1, c0, g.x);      \\\n    }\nreturn true;\n}\n

This gets transformed, by the GLSL macro preprocessor, into an optimized shader template invocation like the following:

{\n// ...\nsh_describe(sh, \"bicubic\");\nconst struct __attribute__((__packed__)) {\nident_t pos;\nident_t tex;\nident_t pt;\nident_t scale;\n} _glsl_330_args = {\n.pos = pos,\n.tex = tex,\n.pt = pt,\n.scale = sh_const_float(sh, \"scale\", scale),\n};\nsize_t _glsl_330_fn(void *, pl_str *, const uint8_t *);\npl_str_builder_append(sh->buffers[SH_BUF_BODY], _glsl_330_fn,\n&_glsl_330_args, sizeof(_glsl_330_args));\n// ...\n}\nsize_t _glsl_330_fn(void *alloc, pl_str *buf, const uint8_t *ptr)\n{\nstruct __attribute__((__packed__)) {\nident_t pos;\nident_t tex;\nident_t pt;\nident_t scale;\n} vars;\nmemcpy(&vars, ptr, sizeof(vars));\npl_str_append_asprintf(alloc, buf,\n\"/* pl_shader_sample_bicubic */\\n\"\n\"    vec4 color;\\n\"\n\"    {\\n\"\n\"    vec2 pos = /*pos*/_%hx;\\n\"\n\"    vec2 size = vec2(textureSize(/*tex*/_%hx, 0));\\n\"\n\"    vec2 frac  = fract(pos * size + vec2(0.5));\\n\"\n\"    vec2 frac2 = frac * frac;\\n\"\n\"    vec2 inv   = vec2(1.0) - frac;\\n\"\n\"    vec2 inv2  = inv * inv;\\n\"\n\"    /* compute basis spline */\\n\"\n\"    vec2 w0 = 1.0/6.0 * inv2 * inv;\\n\"\n\"    vec2 w1 = 2.0/3.0 - 0.5 * frac2 * (2.0 - frac);\\n\"\n\"    vec2 w2 = 2.0/3.0 - 0.5 * inv2  * (2.0 - inv);\\n\"\n\"    vec2 w3 = 1.0/6.0 * frac2 * frac;\\n\"\n\"    vec4 g = vec4(w0 + w1, w2 + w3);\\n\"\n\"    vec4 h = vec4(w1, w3) / g + inv.xyxy;\\n\"\n\"    h.xy -= vec2(2.0);\\n\"\n\"    /* sample four corners, then interpolate */\\n\"\n\"    vec4 p = pos.xyxy + /*pt*/_%hx.xyxy * h;\\n\"\n\"    vec4 c00 = textureLod(/*tex*/_%hx, p.xy, 0.0);\\n\"\n\"    vec4 c01 = textureLod(/*tex*/_%hx, p.xw, 0.0);\\n\"\n\"    vec4 c0 = mix(c01, c00, g.y);\\n\"\n\"    vec4 c10 = textureLod(/*tex*/_%hx, p.zy, 0.0);\\n\"\n\"    vec4 c11 = textureLod(/*tex*/_%hx, p.zw, 0.0);\\n\"\n\"    vec4 c1 = mix(c11, c10, g.y);\\n\"\n\"    color = /*scale*/_%hx * mix(c1, c0, g.x);\\n\"\n\"    }\\n\",\nvars.pos,\nvars.tex,\nvars.pt,\nvars.tex,\nvars.tex,\nvars.tex,\nvars.tex,\nvars.scale\n);\nreturn sizeof(vars);\n}\n

To support this style of shader programming, special syntax was invented:

"},{"location":"glsl/#shader-variables","title":"Shader variables","text":"

Instead of being formatted with \"$\", %f etc. and supplied in a big list, printf style, GLSL macros may directly embed shader variables:

ident_t pos, tex = sh_bind(sh, texture, ..., &pos, ...);\n#pragma GLSL vec4 color = texture($tex, $pos);\n

The simplest possible shader variable is just $name, which corresponds to any variable of type ident_t. More complicated expression are also possible:

#define RAND3 ${sh_prng(sh, false, NULL)}\ncolor.rgb += ${float:params->noise} * RAND3;\n

In the expression ${float:params->noise}, the float: prefix here transforms the shader variable into the equivalent of SH_FLOAT() in the legacy API, that is, a generic float (specialization) constant. Other possible types are:

float f = ${float: M_PI};\nint   i = ${int:   params->width};\nuint  u = ${uint:  sizeof(ssbo)};\n

In addition to a type specifier, the optional qualifiers dynamic and const will modify the variable, turning it into (respectively) a dynamically loaded uniform (SH_FLOAT_DYN etc.), or a hard-coded shader literal (%d, %f etc.):

const float base = ${const float: M_LOG10E};\nint seed = ${dynamic int: rand()};\n
"},{"location":"glsl/#macro-directives","title":"Macro directives","text":"

Lines beginning with @ are not included in the GLSL as-is, but instead parsed as macro directives, to control the code flow inside the macro expansion:

"},{"location":"glsl/#if-else","title":"@if / @else","text":"

Standard-purpose conditional. Example:

float alpha = ...;\n@if (repr.alpha == PL_ALPHA_INDEPENDENT)\ncolor.a *= alpha;\n@else\ncolor.rgba *= alpha;\n

The condition is evaluated outside the macro (in the enclosing scope) and the resulting boolean variable is directly passed to the template.

An @if block can also enclose multiple lines:

@if (threshold > 0) {\nfloat thresh = ${float:threshold};\ncoeff = mix(coeff, vec2(0.0), lessThan(coeff, vec2(thresh)));\ncoeff = mix(coeff, vec2(1.0), greaterThan(coeff, vec2(1.0 - thresh)));\n@}\n
"},{"location":"glsl/#for","title":"@for","text":"

This can be used to generate (unrolled) loops:

int offset = ${const int: params->kernel_width / 2};\nfloat sum = 0.0;\n@for (x < params->kernel_width)\nsum += textureLodOffset($luma, $pos, 0.0, int(@sum - offset)).r;\n

This introduces a local variable, @x, which expands to an integer containing the current loop index. Loop indices always start at 0. Valid terminating conditions include < and <=, and the loop stop condition is also evaluated as an integer.

Alternatively, this can be used to iterate over a bitmask (as commonly used for e.g. components in a color mask):

float weight = /* ... */;\nvec4 color = textureLod($tex, $pos, 0.0);\n@for (c : params->component_mask)\nsum[@c] += weight * color[@c];\n

Finally, to combine loops with conditionals, the special syntax @if @(cond) may be used to evaluate expressions inside the template loop:

@for (i < 10) {\nfloat weight = /* ... */;\n@if @(i < 5)\nweight = -weight;\nsum += weight * texture(...);\n@}\n

In this case, the @if conditional may only reference local (loop) variables.

"},{"location":"glsl/#switch-case","title":"@switch / @case","text":"

This corresponds fairly straightforwardly to a normal switch/case from C:

@switch (color->transfer) {\n@case PL_COLOR_TRC_SRGB:\ncolor.rgb = mix(color.rgb * 1.0/12.92,\npow((color.rgb + vec3(0.055)) / 1.055, vec3(2.4)),\nlessThan(vec3(0.04045), color.rgb));\n@break;\n@case PL_COLOR_TRC_GAMMA18:\ncolor.rgb = pow(color.rgb, vec3(1.8));\n@break;\n@case PL_COLOR_TRC_GAMMA20:\ncolor.rgb = pow(color.rgb, vec3(2.0));\n@break;\n@case PL_COLOR_TRC_GAMMA22:\ncolor.rgb = pow(color.rgb, vec3(2.2));\n@break;\n/* ... */\n@}\n

The switch body is always evaluated as an unsigned int.

"},{"location":"renderer/","title":"Rendering content: pl_frame, pl_renderer, and pl_queue","text":"

This example roughly builds off the previous entry, and as such will not cover the basics of how to create a window, initialize a pl_gpu and get pixels onto the screen.

"},{"location":"renderer/#renderer","title":"Renderer","text":"

The pl_renderer set of APIs represents the highest-level interface into libplacebo, and is what most users who simply want to display e.g. a video feed on-screen will want to be using.

The basic initialization is straightforward, requiring no extra parameters:

pl_renderer renderer;\ninit()\n{\nrenderer = pl_renderer_create(pllog, gpu);\nif (!renderer)\ngoto error;\n// ...\n}\nuninit()\n{\npl_renderer_destroy(&renderer);\n}\n

What makes the renderer powerful is the large number of pl_render_params it exposes. By default, libplacebo provides several presets to use:

  • pl_render_fast_params: Disables everything except for defaults. This is the fastest possible configuration.
  • pl_render_default_params: Contains the recommended default parameters, including some slightly higher quality scaling, as well as dithering.
  • pl_render_high_quality_params: A preset of reasonable defaults for a higher-end machine (i.e. anything with a discrete GPU). This enables most of the basic functionality, including upscaling, downscaling, debanding and better HDR tone mapping.

Covering all of the possible options exposed by pl_render_params is out-of-scope of this example and would be better served by looking at the API documentation.

"},{"location":"renderer/#frames","title":"Frames","text":"

pl_frame is the struct libplacebo uses to group textures and their metadata together into a coherent unit that can be rendered using the renderer. This is not currently a dynamically allocated or refcounted heap object, it is merely a struct that can live on the stack (or anywhere else). The actual data lives in corresponding pl_tex objects referenced in each of the frame's planes.

bool render_frame(const struct pl_frame *image,\nconst struct pl_swapchain_frame *swframe)\n{\nstruct pl_frame target;\npl_frame_from_swapchain(&target, swframe);\nreturn pl_render_image(renderer, image, target,\n&pl_render_default_params);\n}\n

Renderer state

The pl_renderer is conceptually (almost) stateless. The only thing that is needed to get a different result is to change the render params, which can be varied freely on every call, if the user desires.

The one case where this is not entirely true is when using frame mixing (see below), or when using HDR peak detection. In this case, the renderer can be explicitly reset using pl_renderer_flush_cache.

To upload frames, the easiest methods are made available as dedicated helpers in <libplacebo/utils/upload.h>, and <libplacebo/utils/libav.h> (for AVFrames). In general, I recommend checking out the demo programs for a clearer illustration of how to use them in practice.

"},{"location":"renderer/#shader-cache","title":"Shader cache","text":"

The renderer internally generates, compiles and caches a potentially large number of shader programs, some of which can be complex. On some platforms (notably D3D11), these can be quite costly to recompile on every program launch.

As such, the renderer offers a way to save/restore its internal shader cache from some external location (managed by the API user). The use of this API is highly recommended:

static uint8_t *load_saved_cache();\nstatic void store_saved_cache(uint8_t *cache, size_t bytes);\nvoid init()\n{\nrenderer = pl_renderer_create(pllog, gpu);\nif (!renderer)\ngoto error;\nuint8_t *cache = load_saved_cache();\nif (cache) {\npl_renderer_load(renderer, cache);\nfree(cache);\n}\n// ...\n}\nvoid uninit()\n{\nsize_t cache_bytes = pl_renderer_save(renderer, NULL);\nuint8_t *cache = malloc(cache_bytes);\nif (cache) {\npl_renderer_save(renderer, cache);\nstore_saved_cache(cache, cache_bytes);\nfree(cache);\n}\npl_renderer_destroy(&renderer);\n}\n

Cache safety

libplacebo performs only minimal validity checking on the shader cache, and in general, cannot possibly guard against malicious alteration of such files. Loading a cache from an untrusted source represents a remote code execution vector.

"},{"location":"renderer/#frame-mixing","title":"Frame mixing","text":"

One of the renderer's most powerful features is its ability to compensate for differences in framerates between the source and display by using frame mixing to blend adjacent frames together.

Using this API requires presenting the renderer, at each vsync, with a pl_frame_mix struct, describing the current state of the vsync. In principle, such structs can be constructed by hand. To do this, all of the relevant frames (nearby the vsync timestamp) must be collected, and their relative distances to the vsync determined, by normalizing all PTS values such that the vsync represents time 0.0 (and a distance of 1.0 represents the nominal duration between adjacent frames). Note that timing vsyncs, and determining the correct vsync duration, are both left as problems for the user to solve.1. Here could be an example of a valid struct:

(struct pl_frame_mix) {\n.num_frames = 6\n.frames = (const struct pl_frame *[]) {\n/* frame 0 */\n/* frame 1 */\n/* ... */\n/* frame 5 */\n},\n.signatures = (uint64_t[]) {\n0x0, 0x1, 0x2, 0x3, 0x4, 0x5 // (1)\n},\n.timestamps = (float[]) {\n-2.4, -1.4, -0.4, 0.6, 1.6, 2.6, // (2)\n},\n.vsync_duration = 0.4, // 24 fps video on 60 fps display\n}\n
  1. These must be unique per frame, but always refer to the same frame. For example, this could be based on the frame's PTS, the frame's numerical ID (in order of decoding), or some sort of hash. The details don't matter, only that this uniquely identifies specific frames.

  2. Typically, for CFR sources, frame timestamps will always be separated in this list by a distance of 1.0. In this example, the vsync falls roughly halfway (but not quite) in between two adjacent frames (with IDs 0x2 and 0x3).

Frame mixing radius

In this example, the frame mixing radius (as determined by pl_frame_mix_radius is 3.0, so we include all frames that fall within the timestamp interval of [-3, 3). In general, you should consult this function to determine what frames need to be included in the pl_frame_mix - though including more frames than needed is not an error.

"},{"location":"renderer/#frame-queue","title":"Frame queue","text":"

Because this API is rather unwieldy and clumsy to use directly, libplacebo provides a helper abstraction known as pl_queue to assist in transforming some arbitrary source of frames (such as a video decoder) into nicely packed pl_frame_mix structs ready for consumption by the pl_renderer:

#include <libplacebo/utils/frame_queue.h>\npl_queue queue;\nvoid init()\n{\nqueue = pl_queue_create(gpu);\n}\nvoid uninit()\n{\npl_queue_destroy(&queue);\n// ...\n}\n

This queue can be interacted with through a number of mechanisms: either pushing frames (blocking or non-blocking), or by having the queue poll frames (via blocking or non-blocking callback) as-needed. For a full overview of the various methods of pushing and polling frames, check the API documentation.

In this example, I will assume that we have a separate decoder thread pushing frames into the pl_queue in a blocking manner:

static void decoder_thread(void)\n{\nvoid *frame;\nwhile ((frame = /* decode new frame */)) {\npl_queue_push_block(queue, UINT64_MAX, &(struct pl_source_frame) {\n.pts        = /* frame pts */,\n.duration   = /* frame duration */,\n.map        = /* map callback */,\n.unmap      = /* unmap callback */,\n.frame_data = frame,\n});\n}\npl_queue_push(queue, NULL); // signal EOF\n}\n

Now, in our render loop, we want to call pl_queue_update with appropriate values to retrieve the correct frame mix for each vsync:

bool render_frame(const struct pl_swapchain_frame *swframe)\n{\nstruct pl_frame_mix mix;\nenum pl_queue_status res;\nres = pl_queue_update(queue, &mix, pl_queue_params(\n.pts            = /* time of next vsync */,\n.radius         = pl_frame_mix_radius(&render_params),\n.vsync_duration = /* if known */,\n.timeout        = UINT64_MAX, // (2)\n));\nswitch (res) {\ncase PL_QUEUE_OK:\nbreak;\ncase PL_QUEUE_EOF:\n/* no more frames */\nreturn false;\ncase PL_QUEUE_ERR:\ngoto error;\n// (1)\n}\nstruct pl_frame target;\npl_frame_from_swapchain(&target, swframe);\nreturn pl_render_image_mix(renderer, &mix, target,\n&pl_render_default_params);\n}\n
  1. There is a fourth status, PL_QUEUE_MORE, which is returned only if the resulting frame mix is incomplete (and the timeout was reached) - basically this can only happen if the queue runs dry due to frames not being supplied fast enough.

    In this example, since we are setting timeout to UINT64_MAX, we will never get this return value.

  2. Setting this makes pl_queue_update block indefinitely until sufficiently many frames have been pushed into the pl_queue from our separate decoding thread.

"},{"location":"renderer/#deinterlacing","title":"Deinterlacing","text":"

The frame queue also vastly simplifies the process of performing motion-adaptive temporal deinterlacing, by automatically linking together adjacent fields/frames. To take advantage of this, all you need to do is set the appropriate field (pl_source_frame.first_frame), as well as enabling deinterlacing parameters.

  1. However, this may change in the future, as the recent introduction of the Vulkan display timing extension may result in display timing feedback being added to the pl_swapchain API. That said, as of writing, this has not yet happened.\u00a0\u21a9

"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..6d7594fc --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,28 @@ + + + + https://libplacebo.org/ + 2023-08-07 + daily + + + https://libplacebo.org/basic-rendering/ + 2023-08-07 + daily + + + https://libplacebo.org/custom-shaders/ + 2023-08-07 + daily + + + https://libplacebo.org/glsl/ + 2023-08-07 + daily + + + https://libplacebo.org/renderer/ + 2023-08-07 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..683e3110cdb314522d4ee911666efe1ea238c702 GIT binary patch literal 244 zcmV} zrE^;n3Y9^tZ!c|)>DI+fI-6k*CnsMZBTmoOW)BExyer5$=OhECePvu-kn814_v9p3 z;vOtQ%#zt*L7Eu5BV&Ev=V&6ds>9renxSsefb1hOo*#%@WvPaM=`@zGUdtf`Q;aob zXO)K1k0F8$mvgVe*!oIt_-0G_p7LF?e6*QOH_FxU7vLrth9AW?y80{{RK&T(e| literal 0 HcmV?d00001