Yaklaşan WebGPU'ya geçmek, grafik API'lerini değiştirmekten daha fazlası anlamına gelir. Bu aynı zamanda web grafiklerinin geleceğine doğru atılmış bir adımdır. Ancak bu geçiş, hazırlık ve anlayışla daha iyi sonuçlanacaktır ve bu makale sizi buna hazırlayacaktır.
Herkese merhaba, adım Dmitrii Ivashchenko ve MY.GAMES'te yazılım mühendisiyim. Bu makalede WebGL ile gelecek WebGPU arasındaki farkları tartışacağız ve projenizi geçişe nasıl hazırlayacağınızı anlatacağız.
WebGL ve WebGPU'nun Zaman Çizelgesi
WebGPU'nun mevcut durumu ve gelecek
Üst Düzey Kavramsal Farklılıklar
Başlatma
• WebGL: Bağlam Modeli
• WebGPU: Cihaz Modeli
Programlar ve İşlem Hatları
• WebGL: Program
• WebGPU: İşlem Hattı
Üniformalar
• WebGL 1'deki üniformalar
• WebGL 2'deki üniformalar
• WebGPU'daki üniformalar
Gölgelendiriciler
• Gölgelendirici Dili: GLSL ve WGSL
• Veri Türlerinin Karşılaştırılması
• Yapılar
• İşlev Bildirimleri
• Yerleşik işlevler
• Gölgelendirici Dönüşümü
Konvansiyon Farklılıkları
Dokular
• Görünüm Alanı
• Klip Alanları
WebGPU İpuçları ve Püf Noktaları
• Kullandığınız boru hattı sayısını en aza indirin.
• Önceden ardışık düzenler oluşturun
• RenderBundles'ı kullanın
Özet
WebGL'nin diğer birçok web teknolojisi gibi kökleri oldukça eskilere dayanmaktadır. WebGPU'ya geçişin ardındaki dinamikleri ve motivasyonu anlamak için öncelikle WebGL geliştirme geçmişine hızlıca göz atmak faydalı olacaktır:
Son yıllarda geliştiricilere daha fazla kontrol ve esneklik sağlayan yeni grafik API'lerine olan ilgide artış yaşandı:
Bugün WebGPU, sürüm 113'ten başlayarak Google Chrome ve Microsoft Edge tarayıcıları aracılığıyla Windows, Mac ve ChromeOS gibi birden fazla platformda mevcuttur. Yakın gelecekte Linux ve Android desteği bekleniyor.
WebGPU'yu halihazırda destekleyen (veya deneysel destek sunan) motorlardan bazıları şunlardır:
Bunu göz önüne aldığımızda WebGPU'ya geçiş ya da en azından böyle bir geçişe yönelik projeler hazırlamak yakın gelecekte atılmış bir adım gibi görünüyor.
Şimdi uzaklaştıralım ve başlatmadan başlayarak WebGL ile WebGPU arasındaki bazı üst düzey kavramsal farklılıklara bir göz atalım.
Grafik API'leriyle çalışmaya başladığınızda ilk adımlardan biri, ana nesneyi etkileşim için başlatmaktır. Bu süreç, her iki sistem için de bazı özelliklerle birlikte WebGL ve WebGPU arasında farklılık gösterir.
WebGL'de bu nesne "bağlam" olarak bilinir ve esasen bir HTML5 tuval öğesi üzerinde çizim yapmaya yönelik bir arayüzü temsil eder. Bu bağlamı elde etmek oldukça basittir:
const gl = canvas.getContext('webgl');
WebGL'nin içeriği aslında belirli bir tuvale bağlıdır. Bu, birden fazla tuval üzerinde görüntü oluşturmanız gerekiyorsa birden fazla bağlama ihtiyacınız olacağı anlamına gelir.
WebGPU "cihaz" adı verilen yeni bir konsepti tanıtıyor. Bu cihaz etkileşim kuracağınız bir GPU soyutlamasını temsil eder. Başlatma süreci WebGL'ye göre biraz daha karmaşıktır ancak daha fazla esneklik sağlar:
const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const context = canvas.getContext('webgpu'); context.configure({ device, format: 'bgra8unorm', });
Bu modelin avantajlarından biri, bir cihazın birden fazla tuval üzerinde görüntü oluşturabilmesi, hatta hiçbirinde görüntü oluşturamamasıdır. Bu, ek esneklik sağlar; örneğin, bir cihaz birden fazla pencerede veya bağlamda görüntü oluşturmayı kontrol edebilir.
WebGL ve WebGPU, grafik hattını yönetmeye ve organize etmeye yönelik farklı yaklaşımları temsil eder.
WebGL'de ana odak gölgelendirici programıdır. Program köşe ve parça gölgelendiricileri birleştirerek köşelerin nasıl dönüştürülmesi gerektiğini ve her pikselin nasıl renklendirilmesi gerektiğini tanımlar.
const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.bindAttribLocation(program, 'position', 0); gl.linkProgram(program);
WebGL'de program oluşturma adımları:
Bu süreç esnek grafik kontrolüne izin verir, ancak aynı zamanda özellikle büyük ve karmaşık projelerde karmaşık olabilir ve hatalara açık olabilir.
WebGPU, ayrı bir program yerine "boru hattı" kavramını sunar. Bu işlem hattı yalnızca gölgelendiricileri değil aynı zamanda WebGL'de durumlar olarak oluşturulan diğer bilgileri de birleştirir. Dolayısıyla WebGPU'da bir işlem hattı oluşturmak daha karmaşık görünüyor:
const pipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module: shaderModule, entryPoint: 'vertexMain', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }], }, fragment: { module: shaderModule, entryPoint: 'fragmentMain', targets: [{ format, }], }, });
WebGPU'da bir işlem hattı oluşturma adımları:
WebGL, oluşturmanın her yönünü ayırırken, WebGPU daha fazla özelliği tek bir nesnede kapsüllemeye çalışarak sistemi daha modüler ve esnek hale getirir. WebGL'de yapıldığı gibi gölgelendiricileri ve görüntü oluşturma durumlarını ayrı ayrı yönetmek yerine, WebGPU her şeyi tek bir ardışık düzen nesnesinde birleştirir. Bu, süreci daha öngörülebilir ve hatalara daha az eğilimli hale getirir:
Tek tip değişkenler, tüm gölgelendirici örneklerinin kullanabileceği sabit veriler sağlar.
Temel WebGL'de, uniform
değişkenleri doğrudan API çağrıları yoluyla ayarlama olanağımız vardır.
GLSL :
uniform vec3 u_LightPos; uniform vec3 u_LightDir; uniform vec3 u_LightColor;
JavaScript :
const location = gl.getUniformLocation(p, "u_LightPos"); gl.uniform3fv(location, [100, 300, 500]);
Bu yöntem basittir ancak her uniform
değişken için birden fazla API çağrısı gerektirir.
WebGL 2'nin gelişiyle artık uniform
değişkenleri arabellekler halinde gruplama olanağına sahibiz. Yine de ayrı tek tip gölgelendiriciler kullanabilseniz de, daha iyi bir seçenek, tek tip arabellekler kullanarak farklı tek tip gölgelendiricileri daha büyük bir yapıda gruplandırmaktır. Daha sonra tüm bu tek tip verileri, WebGL 1'de köşe arabelleği yüklemeye benzer şekilde GPU'ya aynı anda gönderirsiniz. Bunun, API çağrılarını azaltmak ve modern GPU'ların çalışma şekline daha yakın olmak gibi çeşitli performans avantajları vardır.
GLSL :
layout(std140) uniform ub_Params { vec4 u_LightPos; vec4 u_LightDir; vec4 u_LightColor; };
JavaScript :
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, gl.createBuffer());
WebGL 2'de büyük bir tekdüze arabelleğin alt kümelerini bağlamak için, bindBufferRange
olarak bilinen özel bir API çağrısını kullanabilirsiniz. WebGPU'da, setBindGroup
API'sini çağırırken bir uzaklık listesi iletebileceğiniz, dinamik tekdüze arabellek uzaklıkları adı verilen benzer bir şey vardır.
WebGPU bize daha da iyi bir yöntem sunuyor. Bu bağlamda, bireysel uniform
değişkenler artık desteklenmemektedir ve iş yalnızca uniform
tamponlar aracılığıyla yapılmaktadır.
:
[[block]] struct Params { u_LightPos : vec4<f32>; u_LightColor : vec4<f32>; u_LightDirection : vec4<f32>; }; [[group(0), binding(0)]] var<uniform> ub_Params : Params;
JavaScript :
const buffer = device.createBuffer({ usage: GPUBufferUsage.UNIFORM, size: 8 });
Modern GPU'lar, verilerin birçok küçük blok yerine büyük bir blok halinde yüklenmesini tercih eder. Her seferinde küçük arabellekleri yeniden oluşturmak ve yeniden bağlamak yerine, büyük bir arabellek oluşturmayı ve farklı çizim çağrıları için bunun farklı bölümlerini kullanmayı düşünün. Bu yaklaşım performansı önemli ölçüde artırabilir.
WebGL daha zorunludur; her çağrıda küresel durum sıfırlanır ve mümkün olduğu kadar basit olmaya çabalanır. WebGPU ise daha nesne odaklı olmayı ve kaynakların yeniden kullanımına odaklanmayı hedefliyor, bu da verimliliğe yol açıyor.
WebGL'den WebGPU'ya geçiş yöntem farklılıkları nedeniyle zor görünebilir. Ancak ara adım olarak WebGL 2'ye geçişle başlamak hayatınızı kolaylaştırabilir.
WebGL'den WebGPU'ya geçiş yalnızca API'de değil aynı zamanda gölgelendiricilerde de değişiklik yapılmasını gerektirir. WGSL spesifikasyonu, modern GPU'ların verimliliğini ve performansını korurken bu geçişi sorunsuz ve sezgisel hale getirmek için tasarlanmıştır.
WGSL, WebGPU ile yerel grafik API'leri arasında bir köprü olacak şekilde tasarlanmıştır. GLSL ile karşılaştırıldığında WGSL biraz daha ayrıntılı görünüyor ancak yapısı tanıdık geliyor.
Doku için örnek bir gölgelendirici:
GLSL :
sampler2D myTexture; varying vec2 vTexCoord; void main() { return texture(myTexture, vTexCoord); }
:
[[group(0), binding(0)]] var mySampler: sampler; [[group(0), binding(1)]] var myTexture: texture_2d<f32>; [[stage(fragment)]] fn main([[location(0)]] vTexCoord: vec2<f32>) -> [[location(0)]] vec4<f32> { return textureSample(myTexture, mySampler, vTexCoord); }
Aşağıdaki tablo GLSL ve WGSL'deki temel ve matris veri türlerinin karşılaştırmasını göstermektedir:
GLSL'den WGSL'ye geçiş, kod okunabilirliğini artırabilecek ve hata olasılığını azaltabilecek veri boyutlarının daha sıkı yazılması ve açık bir şekilde tanımlanmasına yönelik isteği gösterir.
Yapıları bildirmenin sözdizimi de değişti:
GLSL:
struct Light { vec3 position; vec4 color; float attenuation; vec3 direction; float innerAngle; float angle; float range; };
- WGSL:
struct Light { position: vec3<f32>, color: vec4<f32>, attenuation: f32, direction: vec3<f32>, innerAngle: f32, angle: f32, range: f32, };
WGSL yapılarında alanların bildirilmesi için açık sözdiziminin getirilmesi, daha fazla netlik isteğini vurgular ve gölgelendiricilerdeki veri yapılarının anlaşılmasını basitleştirir.
GLSL :
float saturate(float x) { return clamp(x, 0.0, 1.0); }
:
fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }
WGSL'deki işlevlerin sözdiziminin değiştirilmesi, bildirimlere ve dönüş değerlerine yönelik yaklaşımın birleştirilmesini yansıtarak kodu daha tutarlı ve öngörülebilir hale getirir.
WGSL'de birçok yerleşik GLSL işlevi yeniden adlandırıldı veya değiştirildi. Örneğin:
WGSL'deki yerleşik işlevleri yeniden adlandırmak yalnızca adlarını basitleştirmekle kalmaz, aynı zamanda onları daha sezgisel hale getirir; bu da diğer grafik API'lerine aşina olan geliştiriciler için geçiş sürecini kolaylaştırabilir.
Projelerini WebGL'den WebGPU'ya dönüştürmeyi planlayanlar için, GLSL'yi otomatik olarak WGSL'ye dönüştürmeye yönelik **[Naga](https://github.com/gfx-rs/naga gibi) araçların bulunduğunu bilmek önemlidir. /)**, GLSL'yi WGSL'ye dönüştürmek için kullanılan bir Rust kütüphanesidir. WebAssembly'ın yardımıyla doğrudan tarayıcınızda bile çalışabilir.
Naga'nın desteklediği uç noktalar şunlardır:
Geçiş sonrasında ters çevrilmiş görseller şeklinde bir sürprizle karşılaşabilirsiniz. Uygulamaları OpenGL'den Direct3D'ye (veya tersi) aktaranlar bu klasik sorunla zaten karşı karşıya kalmışlardır.
OpenGL ve WebGL bağlamında dokular genellikle başlangıç pikseli sol alt köşeye karşılık gelecek şekilde yüklenir. Ancak pratikte birçok geliştirici görselleri sol üst köşeden başlayarak yüklüyor ve bu da ters çevrilmiş görsel hatasına yol açıyor. Bununla birlikte, bu hata diğer faktörlerle telafi edilebilir ve sonuçta sorun ortadan kaldırılabilir.
OpenGL'den farklı olarak Direct3D ve Metal gibi sistemler geleneksel olarak dokuların başlangıç noktası olarak sol üst köşeyi kullanır. Bu yaklaşımın birçok geliştirici için en sezgisel yaklaşım olduğunu göz önünde bulundurarak WebGPU'nun yaratıcıları bu uygulamayı izlemeye karar verdi.
WebGL kodunuz çerçeve arabelleğinden piksel seçiyorsa WebGPU'nun farklı bir koordinat sistemi kullanmasına hazırlıklı olun. Koordinatları düzeltmek için basit bir "y = 1,0 - y" işlemini uygulamanız gerekebilir.
Bir geliştirici, nesnelerin beklenenden daha erken kırpılması veya kaybolması gibi bir sorunla karşılaştığında, bu genellikle derinlik alanındaki farklılıklarla ilgilidir. WebGL ile WebGPU arasında, klip alanının derinlik aralığını tanımlama şekilleri açısından bir fark vardır. WebGL -1 ile 1 arasında bir aralık kullanırken WebGPU, Direct3D, Metal ve Vulkan gibi diğer grafik API'lerine benzer şekilde 0 ile 1 arasında bir aralık kullanır. Bu karar, diğer grafik API'leriyle çalışırken belirlenen 0'dan 1'e kadar bir aralık kullanmanın çeşitli avantajları nedeniyle verildi.
Modelinizin konumlarını klip alanına dönüştürmenin ana sorumluluğu projeksiyon matrisine aittir. Kodunuzu uyarlamanın en basit yolu, projeksiyon matrisinizin çıktılarının 0 ila 1 aralığında olmasını sağlamaktır. gl-matrix gibi kitaplıkları kullananlar için basit bir çözüm var: perspective
işlevini kullanmak yerine, perspectiveZO
; diğer matris işlemleri için de benzer işlevler mevcuttur.
if (webGPU) { // Creates a matrix for a symetric perspective-view frustum // using left-handed coordinates mat4.perspectiveZO(out, Math.PI / 4, ...); } else { // Creates a matrix for a symetric perspective-view frustum // based on the default handedness and default near // and far clip planes definition. mat4.perspective(out, Math.PI / 4, …); }
Ancak bazen elinizde mevcut bir projeksiyon matrisiniz olabilir ve onun kaynağını değiştiremezsiniz. Bu durumda, bunu 0'dan 1'e kadar bir aralığa dönüştürmek için projeksiyon matrisinizi, derinlik aralığını düzelten başka bir matrisle önceden çarpabilirsiniz.
Şimdi WebGPU ile çalışmaya yönelik bazı ipuçlarını ve püf noktalarını tartışalım.
Ne kadar çok işlem hattı kullanırsanız, o kadar çok durum değişimine sahip olursunuz ve performans da o kadar az olur; Varlıklarınızın nereden geldiğine bağlı olarak bu önemsiz olmayabilir.
Bir işlem hattı oluşturmak ve bunu hemen kullanmak işe yarayabilir ancak bu önerilmez. Bunun yerine hemen geri dönen işlevler oluşturun ve farklı bir iş parçacığı üzerinde çalışmaya başlayın. İşlem hattını kullandığınızda yürütme kuyruğunun, bekleyen işlem hattı oluşturma işlemlerinin tamamlanmasını beklemesi gerekir. Bu, önemli performans sorunlarına neden olabilir. Bunu önlemek için işlem hattını oluşturma ile ilk kullanma arasında biraz zaman bıraktığınızdan emin olun.
Veya daha da iyisi, create*PipelineAsync
değişkenlerini kullanın! Söz, herhangi bir duraklama olmadan boru hattı kullanıma hazır olduğunda çözüme kavuşur.
device.createComputePipelineAsync({ compute: { module: shaderModule, entryPoint: 'computeMain' } }).then((pipeline) => { const commandEncoder = device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.dispatchWorkgroups(128); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); });
İşleme paketleri önceden kaydedilmiş, kısmi, yeniden kullanılabilir işleme geçişleridir. Çoğu işleme komutunu içerebilirler (görüntü alanını ayarlama gibi şeyler hariç) ve daha sonra gerçek bir işleme geçişinin parçası olarak "tekrar oynatılabilirler".
const renderPass = encoder.beginRenderPass(descriptor); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.executeBundles([renderBundle]); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.end();
İşleme paketleri, normal işleme geçiş komutlarıyla birlikte yürütülebilir. İşleme geçiş durumu, her paket yürütmesinden önce ve sonra varsayılanlara sıfırlanır. Bu öncelikle çizimin JavaScript yükünü azaltmak için yapılır. Yaklaşımdan bağımsız olarak GPU performansı aynı kalır.
WebGPU'ya geçiş, grafik API'lerini değiştirmekten daha fazlası anlamına gelir. Bu aynı zamanda çeşitli grafik API'lerinin başarılı özelliklerini ve uygulamalarını birleştirerek web grafiklerinin geleceğine doğru atılmış bir adımdır. Bu geçiş, teknik ve felsefi değişikliklerin kapsamlı bir şekilde anlaşılmasını gerektirir, ancak faydaları önemlidir.