Skip to content

Commit 2c70794

Browse files
committed
codegen: support absolute pixel access
1 parent 41388d5 commit 2c70794

7 files changed

Lines changed: 257 additions & 72 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ plugin that allows one to evaluate an expression per pixel.
3838
`boundary` parameter.
3939
- `:c`: Forces clamped boundary.
4040
- `:m`: Forces mirrored boundary.
41+
- Absolute pixel access: `absX absY clip[]:[mode]`.
42+
- Accesses a pixel at an absolute coordinate. It pops `absY` then `absX` from
43+
the stack. These coordinates can be computed by expressions.
44+
- If the coordinates are not integers, they will be rounded half to even.
45+
- **Example:** `X 2 / Y x[]` reads the pixel at half the current X
46+
coordinate from the first clip, using the default clamp mode.
47+
- **Boundary Suffixes:**
48+
- `:c`: Forces clamped boundary.
49+
- `:m`: Forces mirrored boundary.
4150
- Supports any number of input clips. `srcN` may be used to access the `N`-th
4251
input clip. Shorthand aliases `x`, `y`, `z`, `a`, `b`, `c`, etc. map to
4352
`src0`, `src1`, `src2`, `src3`, `src4`, `src5`, etc., up to `w` being `src25`.

src/codegen/compiler.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ fn create_entry_fn(
239239
ModuleError::Compilation(source) => pretty_error(&ctx.func, source),
240240
_ => err.to_string(),
241241
};
242-
panic!("{err_msg}");
242+
return Err(CranexprError::CompilationError(err_msg));
243243
}
244244

245245
// println!("{}", ctx.func.display());
@@ -652,4 +652,58 @@ mod tests {
652652
fn test_bitwise(#[case] expr: &str, #[case] expected: f32) {
653653
assert_relative_eq!(run_expr(expr), expected);
654654
}
655+
656+
#[rstest]
657+
fn test_abs_access() {
658+
// 10 11 12
659+
// 13 14 15
660+
// 16 17 18
661+
let x = [10u8, 11, 12, 13, 14, 15, 16, 17, 18];
662+
663+
let compiled =
664+
compile_jit("1 1 x[]", PixelType::U8, &[PixelType::U8], None).expect("should compile expr");
665+
666+
let mut actual = [0u8; 9];
667+
unsafe {
668+
compiled.invoke(
669+
&mut actual,
670+
&[slice::from_raw_parts(x.as_ptr(), x.len())],
671+
3,
672+
3,
673+
);
674+
};
675+
for v in actual {
676+
assert_eq!(v, 14);
677+
}
678+
}
679+
680+
#[rstest]
681+
fn test_horizontal_flip_u16() {
682+
// 1 2 3
683+
// 4 5 6
684+
// 7 8 9
685+
let x = [1u16, 2, 3, 4, 5, 6, 7, 8, 9];
686+
687+
// 3 2 1
688+
// 6 5 4
689+
// 9 8 7
690+
let expected = [3u16, 2, 1, 6, 5, 4, 9, 8, 7];
691+
692+
let expr = "width X - 1 - Y x[]";
693+
694+
let compiled =
695+
compile_jit(expr, PixelType::U16, &[PixelType::U16], None).expect("should compile expr");
696+
697+
let mut actual = [0u16; 9];
698+
unsafe {
699+
compiled.invoke(
700+
&mut actual,
701+
&[slice::from_raw_parts(x.as_ptr().cast::<u8>(), x.len() * 2)],
702+
3,
703+
3,
704+
);
705+
};
706+
707+
assert_eq!(actual, expected);
708+
}
655709
}

src/codegen/translate.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,12 @@ pub(crate) fn translate_expr(
9191
.variables
9292
.get(name)
9393
.ok_or_else(|| CranexprError::UndefinedVariable(name.clone()))?;
94-
Ok(fx.bcx.use_var(*variable))
94+
let val = fx.bcx.use_var(*variable);
95+
Ok(if fx.bcx.func.dfg.value_type(val) == types::I64 {
96+
fx.bcx.ins().fcvt_from_uint(types::F32, val)
97+
} else {
98+
val
99+
})
95100
}
96101

97102
Expr::IfElse(condition, then_body, else_body) => {
@@ -109,6 +114,57 @@ pub(crate) fn translate_expr(
109114
fx.bcx.def_var(variable, value);
110115
Ok(value)
111116
}
117+
Expr::AbsAccess {
118+
clip,
119+
x,
120+
y,
121+
boundary_mode,
122+
} => {
123+
let x_val = translate_expr(fx, x)?;
124+
let y_val = translate_expr(fx, y)?;
125+
126+
// Resolve clip name to clip index.
127+
let clip_idx = resolve_clip_name(clip, &fx.src_types)?;
128+
let src_type = fx.src_types[clip_idx];
129+
130+
// Round and convert to integer.
131+
let x_nearest = fx.bcx.ins().nearest(x_val);
132+
let y_nearest = fx.bcx.ins().nearest(y_val);
133+
let x_int = fx.bcx.ins().fcvt_to_sint(types::I64, x_nearest);
134+
let y_int = fx.bcx.ins().fcvt_to_sint(types::I64, y_nearest);
135+
136+
// Boundary handling.
137+
let boundary_mode = boundary_mode.unwrap_or(fx.boundary_mode);
138+
let clamped_x = apply_boundary_mode(fx, x_int, fx.width, boundary_mode);
139+
let clamped_y = apply_boundary_mode(fx, y_int, fx.height, boundary_mode);
140+
141+
// idx = y * width + x
142+
let y_times_width = fx.bcx.ins().imul(clamped_y, fx.width);
143+
let target_idx = fx.bcx.ins().iadd(y_times_width, clamped_x);
144+
145+
// Load source pointer for this clip.
146+
let pointer_size = fx.pointer_type.bytes() as i32;
147+
let src_ptr_val = fx
148+
.src_clips
149+
.offset(fx, Offset32::new(clip_idx as i32 * pointer_size))
150+
.load(fx, fx.pointer_type, SRC_MEMFLAGS);
151+
let src_ptr = Pointer::new(src_ptr_val);
152+
153+
// Calculate byte offset for the pixel.
154+
let pixel_offset = fx.bcx.ins().imul_imm(target_idx, src_type.bytes() as i64);
155+
let pixel_ptr = src_ptr.offset_value(fx, pixel_offset);
156+
157+
// Load pixel value.
158+
let val = pixel_ptr.load(fx, src_type.into(), SRC_MEMFLAGS);
159+
160+
// Convert to float.
161+
let val = match src_type {
162+
PixelType::U8 | PixelType::U16 => fx.bcx.ins().fcvt_from_uint(types::F32, val),
163+
PixelType::F32 => val,
164+
};
165+
166+
Ok(val)
167+
}
112168
Expr::RelAccess {
113169
clip,
114170
rel_x,

src/errors.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use thiserror::Error;
99
/// Errors from cranexpr.
1010
#[derive(Debug, Diagnostic, Error)]
1111
pub enum CranexprError {
12+
#[error("Compilation error: {0}")]
13+
CompilationError(String),
14+
1215
#[error("Expression evaluates to nothing.")]
1316
ExpressionEvaluatesToNothing,
1417

src/parser/ast.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,21 @@ pub(crate) enum Expr {
150150
/// Optional boundary mode override.
151151
boundary_mode: Option<BoundaryMode>,
152152
},
153+
154+
/// Absolute pixel access (e.g. `x 100 y 200 src0[]:m`).
155+
AbsAccess {
156+
/// Clip identifier.
157+
clip: String,
158+
159+
/// Absolute X coordinate.
160+
x: Box<Self>,
161+
162+
/// Absolute Y coordinate.
163+
y: Box<Self>,
164+
165+
/// Optional boundary mode override.
166+
///
167+
/// If `None`, the filter's global boundary mode is used.
168+
boundary_mode: Option<BoundaryMode>,
169+
},
153170
}

src/parser/mod.rs

Lines changed: 112 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -48,99 +48,141 @@ pub(crate) fn parse_expr(expr: &str) -> CranexprResult<Vec<Expr>> {
4848
stack.push(Expr::Lit(text.parse::<f32>().unwrap()));
4949
}
5050
TokenKind::Ident => {
51-
// An identifier followed by an open bracket is relative pixel access.
51+
// An identifier followed by an open bracket is relative or absolute pixel access.
5252
if tokens
5353
.peek()
5454
.is_some_and(|(k, _)| *k == TokenKind::OpenBracket)
5555
{
5656
tokens.next(); // Consume `[`.
5757

58-
// Parse relX (must be an integer literal).
59-
let (rel_x_kind, rel_x_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
60-
let rel_x = match rel_x_kind {
61-
TokenKind::Literal { .. } => rel_x_text
62-
.parse::<i32>()
63-
.map_err(|_| CranexprError::StackUnderflow)?,
64-
TokenKind::Minus => {
65-
// Negative integer literal.
66-
let (lit_kind, lit_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
67-
if !matches!(lit_kind, TokenKind::Literal { .. }) {
68-
return Err(CranexprError::StackUnderflow);
69-
}
70-
-(lit_text
71-
.parse::<i32>()
72-
.map_err(|_| CranexprError::StackUnderflow)?)
73-
}
74-
_ => return Err(CranexprError::StackUnderflow),
75-
};
76-
77-
// Skip whitespace and optional comma.
78-
while tokens
58+
// Check for empty brackets `[]` which indicates absolute access.
59+
if tokens
7960
.peek()
80-
.is_some_and(|(k, _)| matches!(k, TokenKind::Whitespace | TokenKind::Comma))
61+
.is_some_and(|(k, _)| *k == TokenKind::CloseBracket)
8162
{
82-
tokens.next();
83-
}
63+
tokens.next(); // Consume `]`.
8464

85-
// Parse relY.
86-
let (rel_y_kind, rel_y_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
87-
let rel_y = match rel_y_kind {
88-
TokenKind::Literal { .. } => rel_y_text
89-
.parse::<i32>()
90-
.map_err(|_| CranexprError::StackUnderflow)?,
91-
TokenKind::Minus => {
92-
let (lit_kind, lit_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
93-
if !matches!(lit_kind, TokenKind::Literal { .. }) {
65+
// Parse optional boundary mode suffix.
66+
let boundary_mode = if tokens.peek().is_some_and(|(k, _)| *k == TokenKind::Colon) {
67+
tokens.next(); // Consume `:`.
68+
// Skip whitespace.
69+
while tokens
70+
.peek()
71+
.is_some_and(|(k, _)| *k == TokenKind::Whitespace)
72+
{
73+
tokens.next();
74+
}
75+
let (mode_kind, mode_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
76+
if mode_kind != TokenKind::Ident {
9477
return Err(CranexprError::StackUnderflow);
9578
}
96-
-(lit_text
79+
match mode_text {
80+
"c" => Some(BoundaryMode::Clamp),
81+
"m" => Some(BoundaryMode::Mirror),
82+
_ => return Err(CranexprError::StackUnderflow),
83+
}
84+
} else {
85+
Some(BoundaryMode::Clamp) // Default to clamp if no suffix
86+
};
87+
88+
let y = stack.pop().ok_or(CranexprError::StackUnderflow)?;
89+
let x = stack.pop().ok_or(CranexprError::StackUnderflow)?;
90+
91+
stack.push(Expr::AbsAccess {
92+
clip: text.to_string(),
93+
x: Box::new(x),
94+
y: Box::new(y),
95+
boundary_mode,
96+
});
97+
} else {
98+
// Relative access parsing
99+
// Parse relX (must be an integer literal).
100+
let (rel_x_kind, rel_x_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
101+
let rel_x = match rel_x_kind {
102+
TokenKind::Literal { .. } => rel_x_text
97103
.parse::<i32>()
98-
.map_err(|_| CranexprError::StackUnderflow)?)
104+
.map_err(|_| CranexprError::StackUnderflow)?,
105+
TokenKind::Minus => {
106+
// Negative integer literal.
107+
let (lit_kind, lit_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
108+
if !matches!(lit_kind, TokenKind::Literal { .. }) {
109+
return Err(CranexprError::StackUnderflow);
110+
}
111+
-(lit_text
112+
.parse::<i32>()
113+
.map_err(|_| CranexprError::StackUnderflow)?)
114+
}
115+
_ => return Err(CranexprError::StackUnderflow),
116+
};
117+
118+
// Skip whitespace and optional comma.
119+
while tokens
120+
.peek()
121+
.is_some_and(|(k, _)| matches!(k, TokenKind::Whitespace | TokenKind::Comma))
122+
{
123+
tokens.next();
99124
}
100-
_ => return Err(CranexprError::StackUnderflow),
101-
};
102125

103-
// Expect close bracket, but skip whitespace first.
104-
while tokens
105-
.peek()
106-
.is_some_and(|(k, _)| *k == TokenKind::Whitespace)
107-
{
108-
tokens.next();
109-
}
110-
let (rbracket_kind, _) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
111-
if rbracket_kind != TokenKind::CloseBracket {
112-
return Err(CranexprError::StackUnderflow);
113-
}
126+
// Parse relY.
127+
let (rel_y_kind, rel_y_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
128+
let rel_y = match rel_y_kind {
129+
TokenKind::Literal { .. } => rel_y_text
130+
.parse::<i32>()
131+
.map_err(|_| CranexprError::StackUnderflow)?,
132+
TokenKind::Minus => {
133+
let (lit_kind, lit_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
134+
if !matches!(lit_kind, TokenKind::Literal { .. }) {
135+
return Err(CranexprError::StackUnderflow);
136+
}
137+
-(lit_text
138+
.parse::<i32>()
139+
.map_err(|_| CranexprError::StackUnderflow)?)
140+
}
141+
_ => return Err(CranexprError::StackUnderflow),
142+
};
114143

115-
// Parse optional boundary mode suffix.
116-
let boundary_override = if tokens.peek().is_some_and(|(k, _)| *k == TokenKind::Colon) {
117-
tokens.next(); // Consume `:`.
118-
// Skip whitespace.
144+
// Expect close bracket, but skip whitespace first.
119145
while tokens
120146
.peek()
121147
.is_some_and(|(k, _)| *k == TokenKind::Whitespace)
122148
{
123149
tokens.next();
124150
}
125-
let (mode_kind, mode_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
126-
if mode_kind != TokenKind::Ident {
151+
let (rbracket_kind, _) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
152+
if rbracket_kind != TokenKind::CloseBracket {
127153
return Err(CranexprError::StackUnderflow);
128154
}
129-
match mode_text {
130-
"c" => Some(BoundaryMode::Clamp),
131-
"m" => Some(BoundaryMode::Mirror),
132-
_ => return Err(CranexprError::StackUnderflow),
133-
}
134-
} else {
135-
None
136-
};
137-
138-
stack.push(Expr::RelAccess {
139-
clip: text.to_string(),
140-
rel_x,
141-
rel_y,
142-
boundary_mode: boundary_override,
143-
});
155+
156+
// Parse optional boundary mode suffix.
157+
let boundary_override = if tokens.peek().is_some_and(|(k, _)| *k == TokenKind::Colon) {
158+
tokens.next(); // Consume `:`.
159+
// Skip whitespace.
160+
while tokens
161+
.peek()
162+
.is_some_and(|(k, _)| *k == TokenKind::Whitespace)
163+
{
164+
tokens.next();
165+
}
166+
let (mode_kind, mode_text) = tokens.next().ok_or(CranexprError::StackUnderflow)?;
167+
if mode_kind != TokenKind::Ident {
168+
return Err(CranexprError::StackUnderflow);
169+
}
170+
match mode_text {
171+
"c" => Some(BoundaryMode::Clamp),
172+
"m" => Some(BoundaryMode::Mirror),
173+
_ => return Err(CranexprError::StackUnderflow),
174+
}
175+
} else {
176+
None
177+
};
178+
179+
stack.push(Expr::RelAccess {
180+
clip: text.to_string(),
181+
rel_x,
182+
rel_y,
183+
boundary_mode: boundary_override,
184+
});
185+
}
144186
}
145187
// An identifier followed by a dot is the start of frame property access.
146188
else if tokens.peek().is_some_and(|(k, _)| *k == TokenKind::Dot) {

0 commit comments

Comments
 (0)