What happened to wgpu's culling?

For me, it seems that Front faced culling is the only thing that works, which makes vertex positions upside down. Otherwise back faced like wgpu-tutorial uses will display nothing, normally the triangle displays upwards as well. (I'm using spirv shaders for context)

We'll need more information to be able help, like code and a screenshot of the problem. I just confirmed that if I swap cull_mode: Some(wgpu::Face::Back) to cull_mode: Some(wgpu::Face::Front) back faces show up instead of front faces as expected.

Also, what do you mean by “upside down”? Culling has no effect on vertex positions. Have you checked that your vertex ordering matches your front_face setting?

use nalgebra::*;
use pollster::FutureExt;
use std::borrow::Borrow;
use std::iter;
use wgpu::{
    include_spirv_raw, Backends, BlendState, ColorTargetState, ColorWrites, CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Face, Features, FragmentState, FrontFace, Instance, InstanceDescriptor, Limits, LoadOp, MemoryHints, MultisampleState, Operations, PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, PowerPreference, PresentMode, PrimitiveState, PrimitiveTopology, Queue, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, RequestAdapterOptions, RequestAdapterOptionsBase, ShaderModuleDescriptorSpirV, StoreOp, Surface, SurfaceConfiguration, SurfaceError, SurfaceTargetUnsafe, TextureAspect, TextureUsages, TextureViewDescriptor, TextureViewDimension, VertexState
};
use winit::{
    application::ApplicationHandler,
    dpi::PhysicalSize,
    event::WindowEvent,
    event_loop::EventLoop,
    window::{Window, WindowAttributes},
    *,
};
fn main() -> anyhow::Result<()> {
    unsafe {
        let el = EventLoop::new().unwrap_unchecked();
        el.set_control_flow(event_loop::ControlFlow::Poll);
        let mut game = Front::new();
        el.run_app(&mut game)?;
    }
    Ok(())
}
struct Back<'a> {
    window: Window,
    instance: Instance,
    device: Device,
    queue: Queue,
    surface: Surface<'a>,
    surface_config: SurfaceConfiguration,
    shaders: Vec<RenderPipeline>,
    pipeline_layout: PipelineLayout,
}
impl<'a> Back<'a> {
    async fn new(window: Window) -> Self {
        unsafe {
            let instance = Instance::new(&InstanceDescriptor {
                backends: Backends::VULKAN,
                ..Default::default()
            });
            let surface = instance
                .create_surface_unsafe(SurfaceTargetUnsafe::from_window(&window).unwrap_unchecked())
                .unwrap_unchecked();
            let adapter = instance
                .request_adapter(&RequestAdapterOptionsBase {
                    power_preference: PowerPreference::HighPerformance,
                    force_fallback_adapter: false,
                    compatible_surface: Some(&surface),
                })
                .await
                .unwrap_unchecked();
            let (device, queue) = adapter
                .request_device(
                    &DeviceDescriptor {
                        label: None,
                        required_features: Features::SPIRV_SHADER_PASSTHROUGH,
                        required_limits: Limits::default(),
                        memory_hints: MemoryHints::Performance,
                    },
                    None,
                )
                .await
                .unwrap_unchecked();
            let cap = surface.get_capabilities(&adapter);
            let mut sf_iter = cap.formats.into_iter();
            let size = window.inner_size();
            let sf_format = sf_iter
                .find(|i| i.is_srgb())
                .unwrap_or(sf_iter.nth(0).unwrap_unchecked());
            let surface_config = SurfaceConfiguration {
                usage: TextureUsages::RENDER_ATTACHMENT,
                format: sf_format,
                width: size.width,
                height: size.height,
                present_mode: cap
                    .present_modes
                    .into_iter()
                    .find(|&i| i == PresentMode::Mailbox)
                    .unwrap_or(PresentMode::Fifo),
                desired_maximum_frame_latency: 2,
                alpha_mode: CompositeAlphaMode::Opaque,
                view_formats: vec![sf_format],
            };
            surface.configure(&device, &surface_config);
            let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
                label: None,
                bind_group_layouts: &[],
                push_constant_ranges: &[],
            });
            window.request_redraw();
            Self {
                instance,
                device,
                queue,
                surface,
                surface_config,
                window,
                pipeline_layout,
                shaders: vec![],
            }
        }
    }
    fn add_shader(&mut self, shader: &ShaderModuleDescriptorSpirV) {
        unsafe {
            let shader = self.device.create_shader_module_spirv(shader);
            self.shaders.push(
                self.device
                    .create_render_pipeline(&RenderPipelineDescriptor {
                        label: None,
                        layout: Some(&self.pipeline_layout),
                        vertex: VertexState {
                            module: &shader,
                            entry_point: Some("vs_main"),
                            compilation_options: Default::default(),
                            buffers: &[],
                        },
                        primitive: PrimitiveState {
                            cull_mode: Some(Face::Back),
                            ..Default::default()
                        },
                        depth_stencil: None,
                        multisample: Default::default(),
                        fragment: Some(FragmentState {
                            module: &shader,
                            entry_point: Some("fs_main"),
                            compilation_options: Default::default(),
                            targets: &[Some(ColorTargetState {
                                format: self.surface_config.format,
                                blend: Some(BlendState::REPLACE),
                                write_mask: ColorWrites::ALL,
                            })],
                        }),
                        multiview: None,
                        cache: None,
                    }),
            )
        }
    }
    fn resize(&mut self, size: PhysicalSize<u32>) {
        if size.width != 0 && size.height != 0 {
            self.surface_config.width = size.width;
            self.surface_config.height = size.height;
            self.surface.configure(&self.device, &self.surface_config);
        }
    }
    fn run(&mut self) -> Result<(), SurfaceError> {
        unsafe {
            let tex = self.surface.get_current_texture()?;
            if tex.suboptimal {
                self.resize((self.surface_config.width,self.surface_config.height).into());
                return Ok(());
            }
            let view = tex.texture.create_view(&TextureViewDescriptor {
                format: Some(self.surface_config.format),
                usage: Some(TextureUsages::RENDER_ATTACHMENT),
                ..Default::default()
            });
            let mut encoder = self
                .device
                .create_command_encoder(&CommandEncoderDescriptor::default());
            {
                let mut rpass = encoder.begin_render_pass(&RenderPassDescriptor {
                    label: None,
                    color_attachments: &[Some(RenderPassColorAttachment {
                        view: &view,
                        resolve_target: None,
                        ops: Operations {
                            load: LoadOp::Load,
                            store: StoreOp::Store,
                        },
                    })],
                    depth_stencil_attachment: None,
                    timestamp_writes: None,
                    occlusion_query_set: None,
                });
                rpass.set_pipeline(&self.shaders[0]);
                rpass.draw(0..3, 0..1);
            }
            self.queue.submit(iter::once(encoder.finish()));
            self.window.pre_present_notify();
            tex.present();
            Ok(())
        }
    }
}
impl<'a> ApplicationHandler for Front<'a> {
    fn window_event(
        &mut self,
        el: &event_loop::ActiveEventLoop,
        window_id: window::WindowId,
        event: event::WindowEvent,
    ) {
        unsafe {
            let state = self.back.as_mut().unwrap_unchecked();
            match event {
                WindowEvent::CloseRequested => {}
                WindowEvent::RedrawRequested => {
                    state.window.request_redraw();
                    match state.run() {
                        Err(SurfaceError::Lost | SurfaceError::Outdated) => {
                            state.resize(
                                (state.surface_config.width, state.surface_config.height).into(),
                            );
                        }
                        Err(SurfaceError::OutOfMemory | SurfaceError::Other) => {
                            el.exit();
                        }
                        _ => {}
                    }
                }
                WindowEvent::Resized(size) => {
                    state.resize(size);
                }
                _ => {}
            }
        }
    }
    fn resumed(&mut self, el: &event_loop::ActiveEventLoop) {
        unsafe {
            let mut back = Back::new(
                el.create_window(WindowAttributes::default().with_title("The Alpha and Omega"))
                    .unwrap_unchecked(),
            )
            .block_on();
            back.add_shader(&include_spirv_raw!("shaders/tri.spv"));
            self.back = Some(back);
        }
    }
}
struct Front<'a> {
    back: Option<Back<'a>>,
}
impl<'a> Front<'a> {
    fn new() -> Self {
        Self { back: None }
    }
}
struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
}

@vertex
fn vs_main(
    @builtin(vertex_index) in_vertex_index: u32,
) -> VertexOutput {
    var out: VertexOutput;
    let x = f32(1 - i32(in_vertex_index)) * 0.5;
    let y = f32(i32(in_vertex_index & 1u) * 2 - 1) * 0.5;
    out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
    return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    return vec4<f32>(0.3, 0.2, 0.1, 1.0);
}

Your vertex shader produces these vertex positions:

( 0.5, -0.5, 0.0, 1.0)
( 0.0,  0.5, 0.0, 1.0)
(-0.5, -0.5, 0.0, 1.0)

Those are top right, bottom middle, top left. That's a clockwise wound triangle, which is backwards from the default (and conventional) counterclockwise winding order, so it counts as a back-face and is culled by your cull_mode: Some(Face::Back).

You should change the sign of x or y in the shader code, for example by swapping this subtraction:

    let x = f32(i32(in_vertex_index) - 1) * 0.5;

Then the triangle will be counterclockwise, and considered a front face.

So then what changed? That shader code is from wgpu-tutorial.

Link?

I assume learn-wgpu.

I just dug into the code there, and I found something interesting: they provide both WGSL and GLSL shaders, but the vertex coordinates are in opposite orders. Yet the code works, in both cases. This must be because in OpenGL, normalized (and viewport) coordinates have the corner with the lowest coordinates be the bottom left corner, not the top left. Thus, a Y flip is needed to be compatible with GLSL shaders’ assumptions. It appears that wgpu does this automatically when loading SPIR-V shaders.

But while your Rust code is specifying a SPIR-V module, the shader code you have shown us is WGSL. How are you creating the SPIR-V? Are you using naga to do it? If so, what code or command line did you use? I suspect the adjust_coordinate_space option needs to be toggled.

(I don’t personally use SPIR-V for anything, so this is all just guessing from the source code.)

I suspect that it's acting this way because my only backend is vulkan, as I never had this issue with backend::ALL. So yeah, I believe I have to use that option.

Oh, and i'm using naga to compile spv so idk how to add that option. Though all i'm saying is that the triangle was rendered perfectly fine without questions a few months ago. Something in wgpu must've changed.

If you mean the naga CLI tool, pass --keep-coordinate-space.

Thank you very much. This kind of help is always appreciated.